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:
@@ -2,6 +2,7 @@ import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/ed
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
import type { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { NodeFactory } from '../../infrastructure/factories/NodeFactory';
|
||||
import { useBTLocale } from '../../hooks/useBTLocale';
|
||||
|
||||
const { Search, X, ChevronDown, ChevronRight } = Icons;
|
||||
|
||||
@@ -35,6 +36,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
onNodeSelect,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useBTLocale();
|
||||
const selectedNodeRef = useRef<HTMLDivElement>(null);
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(false);
|
||||
@@ -52,11 +54,12 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
})
|
||||
: allTemplates;
|
||||
|
||||
const uncategorizedLabel = t('quickCreate.uncategorized');
|
||||
const categoryGroups: CategoryGroup[] = React.useMemo(() => {
|
||||
const groups = new Map<string, NodeTemplate[]>();
|
||||
|
||||
filteredTemplates.forEach((template: NodeTemplate) => {
|
||||
const category = template.category || '未分类';
|
||||
const category = template.category || uncategorizedLabel;
|
||||
if (!groups.has(category)) {
|
||||
groups.set(category, []);
|
||||
}
|
||||
@@ -68,7 +71,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
templates,
|
||||
isExpanded: searchTextLower ? true : expandedCategories.has(category)
|
||||
})).sort((a, b) => a.category.localeCompare(b.category));
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower]);
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower, uncategorizedLabel]);
|
||||
|
||||
const flattenedTemplates = React.useMemo(() => {
|
||||
return categoryGroups.flatMap((group) =>
|
||||
@@ -90,10 +93,10 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (allTemplates.length > 0 && expandedCategories.size === 0) {
|
||||
const categories = new Set(allTemplates.map((t) => t.category || '未分类'));
|
||||
const categories = new Set(allTemplates.map((tmpl) => tmpl.category || uncategorizedLabel));
|
||||
setExpandedCategories(categories);
|
||||
}
|
||||
}, [allTemplates, expandedCategories.size]);
|
||||
}, [allTemplates, expandedCategories.size, uncategorizedLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoScroll && selectedNodeRef.current) {
|
||||
@@ -161,7 +164,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
<Search size={16} style={{ color: '#999', flexShrink: 0 }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索节点..."
|
||||
placeholder={t('quickCreate.searchPlaceholder')}
|
||||
autoFocus
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
@@ -229,7 +232,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
color: '#666',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
未找到匹配的节点
|
||||
{t('quickCreate.noMatchingNodes')}
|
||||
</div>
|
||||
) : (
|
||||
categoryGroups.map((group) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@esengine/editor-runtime';
|
||||
import { useBehaviorTreeDataStore } from '../../stores';
|
||||
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
|
||||
import { BehaviorTreeService } from '../../services/BehaviorTreeService';
|
||||
import { BehaviorTreeServiceToken } from '../../tokens';
|
||||
import { showToast } from '../../services/NotificationService';
|
||||
import { Node as BehaviorTreeNode } from '../../domain/models/Node';
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
@@ -171,7 +171,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
filePath = selected;
|
||||
}
|
||||
|
||||
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||
const service = PluginAPI.resolve(BehaviorTreeServiceToken);
|
||||
await service.saveToFile(filePath);
|
||||
|
||||
setCurrentFilePath(filePath);
|
||||
@@ -205,7 +205,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
if (!selected) return;
|
||||
|
||||
const filePath = selected as string;
|
||||
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||
const service = PluginAPI.resolve(BehaviorTreeServiceToken);
|
||||
await service.loadFromFile(filePath);
|
||||
|
||||
setCurrentFilePath(filePath);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { React, Icons } from '@esengine/editor-runtime';
|
||||
import { useBTLocale } from '../../hooks/useBTLocale';
|
||||
|
||||
const { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } = Icons;
|
||||
|
||||
@@ -43,6 +44,8 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
onCopyToClipboard,
|
||||
onGoToRoot
|
||||
}) => {
|
||||
const { t } = useBTLocale();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -81,7 +84,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="打开文件 (Ctrl+O)"
|
||||
title={t('toolbar.openFile')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -104,7 +107,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title={`保存 (Ctrl+S)${hasUnsavedChanges ? ' - 有未保存的更改' : ''}`}
|
||||
title={hasUnsavedChanges ? t('toolbar.saveUnsaved') : t('toolbar.save')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#1d4ed8' : '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#2563eb' : '#3c3c3c'}
|
||||
>
|
||||
@@ -127,7 +130,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="导出运行时配置"
|
||||
title={t('toolbar.export')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -150,7 +153,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="复制JSON到剪贴板"
|
||||
title={t('toolbar.copyToClipboard')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -192,7 +195,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="运行 (Play)"
|
||||
title={t('toolbar.run')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode !== 'running') {
|
||||
e.currentTarget.style.backgroundColor = '#15803d';
|
||||
@@ -223,7 +226,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title={executionMode === 'paused' ? '继续' : '暂停'}
|
||||
title={executionMode === 'paused' ? t('toolbar.resume') : t('toolbar.pause')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode !== 'idle') {
|
||||
e.currentTarget.style.backgroundColor = '#d97706';
|
||||
@@ -254,7 +257,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="停止"
|
||||
title={t('toolbar.stop')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode !== 'idle') {
|
||||
e.currentTarget.style.backgroundColor = '#b91c1c';
|
||||
@@ -285,7 +288,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="单步执行"
|
||||
title={t('toolbar.step')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode === 'idle' || executionMode === 'paused') {
|
||||
e.currentTarget.style.backgroundColor = '#2563eb';
|
||||
@@ -324,7 +327,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="重置视图 (滚轮缩放, Alt+拖动平移)"
|
||||
title={t('toolbar.resetView')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -362,7 +365,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="撤销 (Ctrl+Z)"
|
||||
title={t('toolbar.undo')}
|
||||
onMouseEnter={(e) => {
|
||||
if (canUndo) {
|
||||
e.currentTarget.style.backgroundColor = '#4a4a4a';
|
||||
@@ -392,7 +395,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
|
||||
title={t('toolbar.redo')}
|
||||
onMouseEnter={(e) => {
|
||||
if (canRedo) {
|
||||
e.currentTarget.style.backgroundColor = '#4a4a4a';
|
||||
@@ -438,8 +441,8 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
color: executionMode === 'running' ? '#16a34a' :
|
||||
executionMode === 'paused' ? '#f59e0b' : '#888'
|
||||
}}>
|
||||
{executionMode === 'idle' ? 'Idle' :
|
||||
executionMode === 'running' ? 'Running' : 'Paused'}
|
||||
{executionMode === 'idle' ? t('execution.idle') :
|
||||
executionMode === 'running' ? t('execution.running') : t('execution.paused')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -465,7 +468,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="回到根节点"
|
||||
title={t('toolbar.goToRoot')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { DomainError } from './DomainError';
|
||||
import { translateBT } from '../../hooks/useBTLocale';
|
||||
|
||||
/**
|
||||
* 验证错误
|
||||
* Validation Error
|
||||
*
|
||||
* 当业务规则验证失败时抛出
|
||||
* Thrown when business rule validation fails
|
||||
*/
|
||||
export class ValidationError extends DomainError {
|
||||
constructor(
|
||||
@@ -15,28 +19,28 @@ export class ValidationError extends DomainError {
|
||||
|
||||
static rootNodeMaxChildren(): ValidationError {
|
||||
return new ValidationError(
|
||||
'根节点只能连接一个子节点',
|
||||
translateBT('validation.rootNodeMaxChildren'),
|
||||
'children'
|
||||
);
|
||||
}
|
||||
|
||||
static decoratorNodeMaxChildren(): ValidationError {
|
||||
return new ValidationError(
|
||||
'装饰节点只能连接一个子节点',
|
||||
translateBT('validation.decoratorNodeMaxChildren'),
|
||||
'children'
|
||||
);
|
||||
}
|
||||
|
||||
static leafNodeNoChildren(): ValidationError {
|
||||
return new ValidationError(
|
||||
'叶子节点不能有子节点',
|
||||
translateBT('validation.leafNodeNoChildren'),
|
||||
'children'
|
||||
);
|
||||
}
|
||||
|
||||
static circularReference(nodeId: string): ValidationError {
|
||||
return new ValidationError(
|
||||
`检测到循环引用,节点 ${nodeId} 不能连接到自己或其子节点`,
|
||||
translateBT('validation.circularReference', undefined, { nodeId }),
|
||||
'connection',
|
||||
nodeId
|
||||
);
|
||||
@@ -44,7 +48,7 @@ export class ValidationError extends DomainError {
|
||||
|
||||
static invalidConnection(from: string, to: string, reason: string): ValidationError {
|
||||
return new ValidationError(
|
||||
`无效的连接:${reason}`,
|
||||
translateBT('validation.invalidConnection', undefined, { reason }),
|
||||
'connection',
|
||||
{ from, to }
|
||||
);
|
||||
|
||||
@@ -2,3 +2,4 @@ export { useCommandHistory } from './useCommandHistory';
|
||||
export { useNodeOperations } from './useNodeOperations';
|
||||
export { useConnectionOperations } from './useConnectionOperations';
|
||||
export { useCanvasInteraction } from './useCanvasInteraction';
|
||||
export { useBTLocale, translateBT } from './useBTLocale';
|
||||
|
||||
73
packages/behavior-tree-editor/src/hooks/useBTLocale.ts
Normal file
73
packages/behavior-tree-editor/src/hooks/useBTLocale.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Behavior Tree Editor Locale Hook
|
||||
* 行为树编辑器语言钩子
|
||||
*
|
||||
* Uses the unified plugin i18n infrastructure from editor-runtime.
|
||||
* 使用 editor-runtime 的统一插件国际化基础设施。
|
||||
*/
|
||||
import {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale
|
||||
} from '@esengine/editor-runtime';
|
||||
import { en, zh, es } from '../locales';
|
||||
import type { Locale, TranslationParams } from '@esengine/editor-core';
|
||||
|
||||
// Create translations bundle
|
||||
// 创建翻译包
|
||||
const translations = { en, zh, es };
|
||||
|
||||
/**
|
||||
* Hook for accessing behavior tree editor translations
|
||||
* 访问行为树编辑器翻译的 Hook
|
||||
*
|
||||
* Uses the unified createPluginLocale factory from editor-runtime.
|
||||
* 使用 editor-runtime 的统一 createPluginLocale 工厂。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { t, locale } = useBTLocale();
|
||||
* return <button title={t('toolbar.save')}>{t('toolbar.saveUnsaved')}</button>;
|
||||
* ```
|
||||
*/
|
||||
export const useBTLocale = createPluginLocale(translations);
|
||||
|
||||
// Create non-React translator using the unified infrastructure
|
||||
// 使用统一基础设施创建非 React 翻译器
|
||||
const btTranslator = createPluginTranslator(translations);
|
||||
|
||||
/**
|
||||
* Non-React translation function for behavior tree editor
|
||||
* 行为树编辑器的非 React 翻译函数
|
||||
*
|
||||
* Use this in services, utilities, and other non-React contexts.
|
||||
* 在服务、工具类和其他非 React 上下文中使用。
|
||||
*
|
||||
* @param key - Translation key | 翻译键
|
||||
* @param locale - Optional locale, defaults to current locale | 可选语言,默认使用当前语言
|
||||
* @param params - Optional interpolation parameters | 可选插值参数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit locale
|
||||
* translateBT('errors.notFound', 'zh');
|
||||
*
|
||||
* // With current locale (auto-detected)
|
||||
* translateBT('errors.notFound');
|
||||
*
|
||||
* // With parameters
|
||||
* translateBT('messages.saved', undefined, { name: 'MyTree' });
|
||||
* ```
|
||||
*/
|
||||
export function translateBT(
|
||||
key: string,
|
||||
locale?: Locale,
|
||||
params?: TranslationParams
|
||||
): string {
|
||||
const targetLocale = locale || getCurrentLocale();
|
||||
return btTranslator(key, targetLocale, params);
|
||||
}
|
||||
|
||||
// Re-export for external use
|
||||
// 重新导出供外部使用
|
||||
export { getCurrentLocale } from '@esengine/editor-runtime';
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type IFileSystem,
|
||||
createLogger,
|
||||
PluginAPI,
|
||||
LocaleService,
|
||||
} from '@esengine/editor-runtime';
|
||||
|
||||
// Runtime imports from @esengine/behavior-tree package
|
||||
@@ -35,6 +36,7 @@ import { BehaviorTreeRuntimeComponent, BehaviorTreeRuntimeModule } from '@esengi
|
||||
// Editor components and services
|
||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
||||
import { FileSystemService } from './services/FileSystemService';
|
||||
import { BehaviorTreeServiceToken, type IBehaviorTreeService } from './tokens';
|
||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
||||
@@ -45,6 +47,9 @@ import { PluginContext } from './PluginContext';
|
||||
// Import manifest from local file
|
||||
import { manifest } from './BehaviorTreePlugin';
|
||||
|
||||
// Import locale translations
|
||||
import { en, zh, es } from './locales';
|
||||
|
||||
// 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM)
|
||||
// Import editor CSS styles (automatically handled and injected by vite)
|
||||
import './styles/BehaviorTreeNode.css';
|
||||
@@ -81,6 +86,9 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
// 订阅创建资产消息
|
||||
this.subscribeToMessages(services);
|
||||
|
||||
// 注册翻译 | Register translations
|
||||
this.registerTranslations(services);
|
||||
|
||||
logger.info('BehaviorTree editor module installed');
|
||||
}
|
||||
|
||||
@@ -173,7 +181,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
|
||||
if (this.services) {
|
||||
this.services.unregister(FileSystemService);
|
||||
this.services.unregister(BehaviorTreeService);
|
||||
this.services.unregister(BehaviorTreeServiceToken.id);
|
||||
}
|
||||
|
||||
useBehaviorTreeDataStore.getState().reset();
|
||||
@@ -190,11 +198,16 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
}
|
||||
services.registerSingleton(FileSystemService);
|
||||
|
||||
// BehaviorTreeService
|
||||
if (services.isRegistered(BehaviorTreeService)) {
|
||||
services.unregister(BehaviorTreeService);
|
||||
// BehaviorTreeService - 使用 ServiceToken.id (symbol) 注册
|
||||
// BehaviorTreeService - register with ServiceToken.id (symbol)
|
||||
// ServiceContainer 支持 symbol 作为 ServiceIdentifier
|
||||
// ServiceContainer supports symbol as ServiceIdentifier
|
||||
const tokenId = BehaviorTreeServiceToken.id;
|
||||
if (services.isRegistered(tokenId)) {
|
||||
services.unregister(tokenId);
|
||||
}
|
||||
services.registerSingleton(BehaviorTreeService);
|
||||
const btService = new BehaviorTreeService();
|
||||
services.registerInstance(tokenId, btService);
|
||||
}
|
||||
|
||||
private registerCompilers(services: ServiceContainer): void {
|
||||
@@ -208,6 +221,22 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件翻译到 LocaleService
|
||||
* Register plugin translations to LocaleService
|
||||
*/
|
||||
private registerTranslations(services: ServiceContainer): void {
|
||||
try {
|
||||
const localeService = services.tryResolve<LocaleService>(LocaleService);
|
||||
if (localeService) {
|
||||
localeService.extendTranslations('behaviorTree', { en, zh, es });
|
||||
logger.info('BehaviorTree translations registered');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to register translations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private registerInspectorProviders(services: ServiceContainer): void {
|
||||
try {
|
||||
const inspectorRegistry = services.resolve<InspectorRegistry>(IInspectorRegistry);
|
||||
@@ -284,7 +313,9 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
extensions: ['btree'],
|
||||
onDoubleClick: async (filePath: string) => {
|
||||
if (this.services) {
|
||||
const service = this.services.resolve(BehaviorTreeService);
|
||||
// 使用 ServiceToken.id 解析服务
|
||||
// Resolve service using ServiceToken.id
|
||||
const service = this.services.resolve<IBehaviorTreeService>(BehaviorTreeServiceToken.id);
|
||||
if (service) {
|
||||
await service.loadFromFile(filePath);
|
||||
}
|
||||
@@ -351,6 +382,7 @@ export { BehaviorTreeRuntimeModule };
|
||||
export { PluginContext } from './PluginContext';
|
||||
export { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
||||
export * from './services/BehaviorTreeService';
|
||||
export * from './tokens';
|
||||
export * from './providers/BehaviorTreeNodeInspectorProvider';
|
||||
|
||||
export * from './domain';
|
||||
|
||||
@@ -2,9 +2,11 @@ import { IValidator, ValidationResult, ValidationError } from '../../domain/inte
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
import { Connection } from '../../domain/models/Connection';
|
||||
import { translateBT } from '../../hooks/useBTLocale';
|
||||
|
||||
/**
|
||||
* 行为树验证器实现
|
||||
* Behavior Tree Validator Implementation
|
||||
*/
|
||||
export class BehaviorTreeValidator implements IValidator {
|
||||
/**
|
||||
@@ -37,21 +39,22 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
|
||||
/**
|
||||
* 验证节点
|
||||
* Validate node
|
||||
*/
|
||||
validateNode(node: Node): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// 验证节点必填字段
|
||||
// 验证节点必填字段 | Validate required fields
|
||||
if (!node.id) {
|
||||
errors.push({
|
||||
message: '节点 ID 不能为空',
|
||||
message: translateBT('validation.nodeIdRequired'),
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
|
||||
if (!node.template) {
|
||||
errors.push({
|
||||
message: '节点模板不能为空',
|
||||
message: translateBT('validation.nodeTemplateRequired'),
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
@@ -64,30 +67,31 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
|
||||
/**
|
||||
* 验证连接
|
||||
* Validate connection
|
||||
*/
|
||||
validateConnection(connection: Connection, tree: BehaviorTree): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// 验证连接的源节点和目标节点都存在
|
||||
// 验证连接的源节点和目标节点都存在 | Validate source and target nodes exist
|
||||
const fromNode = tree.nodes.find((n) => n.id === connection.from);
|
||||
const toNode = tree.nodes.find((n) => n.id === connection.to);
|
||||
|
||||
if (!fromNode) {
|
||||
errors.push({
|
||||
message: `连接的源节点不存在: ${connection.from}`
|
||||
message: translateBT('validation.sourceNodeNotFound', undefined, { nodeId: connection.from })
|
||||
});
|
||||
}
|
||||
|
||||
if (!toNode) {
|
||||
errors.push({
|
||||
message: `连接的目标节点不存在: ${connection.to}`
|
||||
message: translateBT('validation.targetNodeNotFound', undefined, { nodeId: connection.to })
|
||||
});
|
||||
}
|
||||
|
||||
// 不能自己连接自己
|
||||
// 不能自己连接自己 | Cannot connect to self
|
||||
if (connection.from === connection.to) {
|
||||
errors.push({
|
||||
message: '节点不能连接到自己',
|
||||
message: translateBT('validation.selfConnection'),
|
||||
nodeId: connection.from
|
||||
});
|
||||
}
|
||||
@@ -100,6 +104,7 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
|
||||
/**
|
||||
* 验证是否会产生循环引用
|
||||
* Validate no cycles exist
|
||||
*/
|
||||
validateNoCycles(tree: BehaviorTree): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
@@ -134,7 +139,7 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
for (const node of tree.nodes) {
|
||||
if (hasCycle(node.id)) {
|
||||
errors.push({
|
||||
message: '行为树中存在循环引用',
|
||||
message: translateBT('validation.cycleDetected'),
|
||||
nodeId: node.id
|
||||
});
|
||||
break;
|
||||
|
||||
137
packages/behavior-tree-editor/src/locales/en.ts
Normal file
137
packages/behavior-tree-editor/src/locales/en.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* English translations for Behavior Tree Editor
|
||||
* 行为树编辑器英文翻译
|
||||
*/
|
||||
export const en = {
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
openFile: 'Open File (Ctrl+O)',
|
||||
save: 'Save (Ctrl+S)',
|
||||
saveUnsaved: 'Save (Ctrl+S) - Unsaved changes',
|
||||
export: 'Export Runtime Config',
|
||||
copyToClipboard: 'Copy JSON to Clipboard',
|
||||
run: 'Run (Play)',
|
||||
resume: 'Resume',
|
||||
pause: 'Pause',
|
||||
stop: 'Stop',
|
||||
step: 'Step',
|
||||
resetView: 'Reset View (scroll to zoom, Alt+drag to pan)',
|
||||
undo: 'Undo (Ctrl+Z)',
|
||||
redo: 'Redo (Ctrl+Shift+Z / Ctrl+Y)',
|
||||
goToRoot: 'Go to Root Node'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Execution Status
|
||||
// ========================================
|
||||
execution: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Node
|
||||
// ========================================
|
||||
node: {
|
||||
executionOrder: 'Execution Order: {{order}}',
|
||||
initialValue: 'Initial Value',
|
||||
currentValue: 'Current Value'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Context Menu
|
||||
// ========================================
|
||||
contextMenu: {
|
||||
delete: 'Delete',
|
||||
duplicate: 'Duplicate',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Quick Create Menu
|
||||
// ========================================
|
||||
quickCreate: {
|
||||
searchPlaceholder: 'Search nodes...',
|
||||
uncategorized: 'Uncategorized',
|
||||
noMatchingNodes: 'No matching nodes found'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blackboard Panel
|
||||
// ========================================
|
||||
blackboard: {
|
||||
title: 'Blackboard',
|
||||
variableName: 'variable.name',
|
||||
copy: 'Copy',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
addVariable: 'Add Variable'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Compiler
|
||||
// ========================================
|
||||
compiler: {
|
||||
name: 'Behavior Tree Compiler',
|
||||
description: 'Compile behavior tree assets',
|
||||
selectAssetOutput: 'Select asset output directory...',
|
||||
selectTypeOutput: 'Select type definition output directory...',
|
||||
compile: 'Compile',
|
||||
compiling: 'Compiling...',
|
||||
success: 'Compilation successful',
|
||||
failed: 'Compilation failed'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: 'File saved: {{path}}',
|
||||
fileSaveFailed: 'Failed to save file',
|
||||
fileOpened: 'File opened: {{path}}',
|
||||
fileOpenFailed: 'Failed to open file',
|
||||
copiedToClipboard: 'Copied to clipboard',
|
||||
exportSuccess: 'Export successful',
|
||||
exportFailed: 'Export failed',
|
||||
validationError: 'Validation error: {{message}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
createBehaviorTree: 'Create Behavior Tree Asset',
|
||||
confirmDelete: 'Are you sure you want to delete this node?',
|
||||
unsavedChanges: 'You have unsaved changes. Do you want to save before closing?'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Behavior Tree Editor',
|
||||
noFileOpen: 'No behavior tree file is open',
|
||||
dropToOpen: 'Drop a .btree file here or use Open button'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Validation Errors
|
||||
// ========================================
|
||||
validation: {
|
||||
rootNodeMaxChildren: 'Root node can only connect to one child node',
|
||||
decoratorNodeMaxChildren: 'Decorator node can only connect to one child node',
|
||||
leafNodeNoChildren: 'Leaf node cannot have children',
|
||||
circularReference: 'Circular reference detected, node {{nodeId}} cannot connect to itself or its descendants',
|
||||
invalidConnection: 'Invalid connection: {{reason}}',
|
||||
nodeIdRequired: 'Node ID cannot be empty',
|
||||
nodeTemplateRequired: 'Node template cannot be empty',
|
||||
sourceNodeNotFound: 'Connection source node not found: {{nodeId}}',
|
||||
targetNodeNotFound: 'Connection target node not found: {{nodeId}}',
|
||||
selfConnection: 'Node cannot connect to itself',
|
||||
cycleDetected: 'Circular reference detected in behavior tree'
|
||||
}
|
||||
};
|
||||
138
packages/behavior-tree-editor/src/locales/es.ts
Normal file
138
packages/behavior-tree-editor/src/locales/es.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Spanish translations for Behavior Tree Editor
|
||||
* Traducciones en español del Editor de Árbol de Comportamiento
|
||||
*/
|
||||
export const es = {
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
openFile: 'Abrir Archivo (Ctrl+O)',
|
||||
save: 'Guardar (Ctrl+S)',
|
||||
saveUnsaved: 'Guardar (Ctrl+S) - Cambios sin guardar',
|
||||
export: 'Exportar Configuración de Ejecución',
|
||||
copyToClipboard: 'Copiar JSON al Portapapeles',
|
||||
run: 'Ejecutar (Play)',
|
||||
resume: 'Continuar',
|
||||
pause: 'Pausar',
|
||||
stop: 'Detener',
|
||||
step: 'Paso a Paso',
|
||||
resetView: 'Restablecer Vista (scroll para zoom, Alt+arrastrar para desplazar)',
|
||||
undo: 'Deshacer (Ctrl+Z)',
|
||||
redo: 'Rehacer (Ctrl+Shift+Z / Ctrl+Y)',
|
||||
goToRoot: 'Ir al Nodo Raíz'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Execution Status
|
||||
// ========================================
|
||||
execution: {
|
||||
idle: 'Inactivo',
|
||||
running: 'Ejecutando',
|
||||
paused: 'Pausado'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Node
|
||||
// ========================================
|
||||
node: {
|
||||
executionOrder: 'Orden de Ejecución: {{order}}',
|
||||
initialValue: 'Valor Inicial',
|
||||
currentValue: 'Valor Actual'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Context Menu
|
||||
// ========================================
|
||||
contextMenu: {
|
||||
delete: 'Eliminar',
|
||||
duplicate: 'Duplicar',
|
||||
copy: 'Copiar',
|
||||
paste: 'Pegar'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Quick Create Menu
|
||||
// ========================================
|
||||
quickCreate: {
|
||||
searchPlaceholder: 'Buscar nodos...',
|
||||
uncategorized: 'Sin categoría',
|
||||
noMatchingNodes: 'No se encontraron nodos coincidentes'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blackboard Panel
|
||||
// ========================================
|
||||
blackboard: {
|
||||
title: 'Pizarra',
|
||||
variableName: 'nombre.variable',
|
||||
copy: 'Copiar',
|
||||
edit: 'Editar',
|
||||
delete: 'Eliminar',
|
||||
addVariable: 'Agregar Variable'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Compiler
|
||||
// ========================================
|
||||
compiler: {
|
||||
name: 'Compilador de Árbol de Comportamiento',
|
||||
description: 'Compilar recursos de árbol de comportamiento',
|
||||
selectAssetOutput: 'Seleccionar directorio de salida de recursos...',
|
||||
selectTypeOutput: 'Seleccionar directorio de salida de definiciones de tipo...',
|
||||
compile: 'Compilar',
|
||||
compiling: 'Compilando...',
|
||||
success: 'Compilación exitosa',
|
||||
failed: 'Compilación fallida'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: 'Archivo guardado: {{path}}',
|
||||
fileSaveFailed: 'Error al guardar archivo',
|
||||
fileOpened: 'Archivo abierto: {{path}}',
|
||||
fileOpenFailed: 'Error al abrir archivo',
|
||||
copiedToClipboard: 'Copiado al portapapeles',
|
||||
exportSuccess: 'Exportación exitosa',
|
||||
exportFailed: 'Exportación fallida',
|
||||
validationError: 'Error de validación: {{message}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
createBehaviorTree: 'Crear Recurso de Árbol de Comportamiento',
|
||||
confirmDelete: '¿Está seguro de que desea eliminar este nodo?',
|
||||
unsavedChanges: 'Tiene cambios sin guardar. ¿Desea guardar antes de cerrar?'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Editor de Árbol de Comportamiento',
|
||||
noFileOpen: 'No hay archivo de árbol de comportamiento abierto',
|
||||
dropToOpen: 'Arrastre un archivo .btree aquí o use el botón Abrir'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Validation Errors
|
||||
// Errores de validación
|
||||
// ========================================
|
||||
validation: {
|
||||
rootNodeMaxChildren: 'El nodo raíz solo puede conectar a un nodo hijo',
|
||||
decoratorNodeMaxChildren: 'El nodo decorador solo puede conectar a un nodo hijo',
|
||||
leafNodeNoChildren: 'El nodo hoja no puede tener hijos',
|
||||
circularReference: 'Referencia circular detectada, el nodo {{nodeId}} no puede conectarse a sí mismo o a sus descendientes',
|
||||
invalidConnection: 'Conexión inválida: {{reason}}',
|
||||
nodeIdRequired: 'El ID del nodo no puede estar vacío',
|
||||
nodeTemplateRequired: 'La plantilla del nodo no puede estar vacía',
|
||||
sourceNodeNotFound: 'Nodo fuente de conexión no encontrado: {{nodeId}}',
|
||||
targetNodeNotFound: 'Nodo destino de conexión no encontrado: {{nodeId}}',
|
||||
selfConnection: 'El nodo no puede conectarse a sí mismo',
|
||||
cycleDetected: 'Referencia circular detectada en el árbol de comportamiento'
|
||||
}
|
||||
};
|
||||
9
packages/behavior-tree-editor/src/locales/index.ts
Normal file
9
packages/behavior-tree-editor/src/locales/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Behavior Tree Editor Locales
|
||||
* 行为树编辑器多语言支持
|
||||
*/
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
|
||||
export type BehaviorTreeTranslations = typeof import('./en').en;
|
||||
138
packages/behavior-tree-editor/src/locales/zh.ts
Normal file
138
packages/behavior-tree-editor/src/locales/zh.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Chinese translations for Behavior Tree Editor
|
||||
* 行为树编辑器中文翻译
|
||||
*/
|
||||
export const zh = {
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
openFile: '打开文件 (Ctrl+O)',
|
||||
save: '保存 (Ctrl+S)',
|
||||
saveUnsaved: '保存 (Ctrl+S) - 有未保存的更改',
|
||||
export: '导出运行时配置',
|
||||
copyToClipboard: '复制JSON到剪贴板',
|
||||
run: '运行 (Play)',
|
||||
resume: '继续',
|
||||
pause: '暂停',
|
||||
stop: '停止',
|
||||
step: '单步执行',
|
||||
resetView: '重置视图 (滚轮缩放, Alt+拖动平移)',
|
||||
undo: '撤销 (Ctrl+Z)',
|
||||
redo: '重做 (Ctrl+Shift+Z / Ctrl+Y)',
|
||||
goToRoot: '回到根节点'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Execution Status
|
||||
// ========================================
|
||||
execution: {
|
||||
idle: '空闲',
|
||||
running: '运行中',
|
||||
paused: '已暂停'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Node
|
||||
// ========================================
|
||||
node: {
|
||||
executionOrder: '执行顺序: {{order}}',
|
||||
initialValue: '初始值',
|
||||
currentValue: '当前值'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Context Menu
|
||||
// ========================================
|
||||
contextMenu: {
|
||||
delete: '删除',
|
||||
duplicate: '复制',
|
||||
copy: '复制',
|
||||
paste: '粘贴'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Quick Create Menu
|
||||
// ========================================
|
||||
quickCreate: {
|
||||
searchPlaceholder: '搜索节点...',
|
||||
uncategorized: '未分类',
|
||||
noMatchingNodes: '未找到匹配的节点'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blackboard Panel
|
||||
// ========================================
|
||||
blackboard: {
|
||||
title: '黑板',
|
||||
variableName: '变量名',
|
||||
copy: '复制',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
addVariable: '添加变量'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Compiler
|
||||
// ========================================
|
||||
compiler: {
|
||||
name: '行为树编译器',
|
||||
description: '编译行为树资产',
|
||||
selectAssetOutput: '选择资产输出目录...',
|
||||
selectTypeOutput: '选择类型定义输出目录...',
|
||||
compile: '编译',
|
||||
compiling: '编译中...',
|
||||
success: '编译成功',
|
||||
failed: '编译失败'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: '文件已保存: {{path}}',
|
||||
fileSaveFailed: '保存文件失败',
|
||||
fileOpened: '文件已打开: {{path}}',
|
||||
fileOpenFailed: '打开文件失败',
|
||||
copiedToClipboard: '已复制到剪贴板',
|
||||
exportSuccess: '导出成功',
|
||||
exportFailed: '导出失败',
|
||||
validationError: '验证错误: {{message}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
createBehaviorTree: '创建行为树资产',
|
||||
confirmDelete: '确定要删除这个节点吗?',
|
||||
unsavedChanges: '有未保存的更改。关闭前是否保存?'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: '行为树编辑器',
|
||||
noFileOpen: '没有打开的行为树文件',
|
||||
dropToOpen: '拖放 .btree 文件到这里或使用打开按钮'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Validation Errors
|
||||
// 验证错误
|
||||
// ========================================
|
||||
validation: {
|
||||
rootNodeMaxChildren: '根节点只能连接一个子节点',
|
||||
decoratorNodeMaxChildren: '装饰节点只能连接一个子节点',
|
||||
leafNodeNoChildren: '叶子节点不能有子节点',
|
||||
circularReference: '检测到循环引用,节点 {{nodeId}} 不能连接到自己或其子节点',
|
||||
invalidConnection: '无效的连接:{{reason}}',
|
||||
nodeIdRequired: '节点 ID 不能为空',
|
||||
nodeTemplateRequired: '节点模板不能为空',
|
||||
sourceNodeNotFound: '连接的源节点不存在: {{nodeId}}',
|
||||
targetNodeNotFound: '连接的目标节点不存在: {{nodeId}}',
|
||||
selfConnection: '节点不能连接到自己',
|
||||
cycleDetected: '行为树中存在循环引用'
|
||||
}
|
||||
};
|
||||
@@ -29,7 +29,9 @@ const PropertyEditor: React.FC<PropertyEditorProps> = ({ property, value, onChan
|
||||
const renderInput = () => {
|
||||
// 特殊处理 treeAssetId 字段使用 asset 编辑器
|
||||
if (property.name === 'treeAssetId') {
|
||||
const fieldRegistry = PluginAPI.resolve<FieldEditorRegistry>(FieldEditorRegistry);
|
||||
// 使用 ServiceContainer.resolve 直接获取类注册的服务
|
||||
// Use ServiceContainer.resolve to get class-registered service directly
|
||||
const fieldRegistry = PluginAPI.services.resolve(FieldEditorRegistry);
|
||||
const assetEditor = fieldRegistry.getEditor('asset');
|
||||
|
||||
if (assetEditor) {
|
||||
@@ -52,7 +54,9 @@ const PropertyEditor: React.FC<PropertyEditorProps> = ({ property, value, onChan
|
||||
|
||||
// 检查是否有特定的字段编辑器类型
|
||||
if (property.fieldEditor) {
|
||||
const fieldRegistry = PluginAPI.resolve<FieldEditorRegistry>(FieldEditorRegistry);
|
||||
// 使用 ServiceContainer.resolve 直接获取类注册的服务
|
||||
// Use ServiceContainer.resolve to get class-registered service directly
|
||||
const fieldRegistry = PluginAPI.services.resolve(FieldEditorRegistry);
|
||||
const editor = fieldRegistry.getEditor(property.fieldEditor.type);
|
||||
|
||||
if (editor) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
singleton,
|
||||
type IService,
|
||||
createLogger,
|
||||
MessageHub,
|
||||
IMessageHub,
|
||||
@@ -9,11 +8,12 @@ import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataS
|
||||
import type { BehaviorTree } from '../domain/models/BehaviorTree';
|
||||
import { FileSystemService } from './FileSystemService';
|
||||
import { PluginContext } from '../PluginContext';
|
||||
import type { IBehaviorTreeService } from '../tokens';
|
||||
|
||||
const logger = createLogger('BehaviorTreeService');
|
||||
|
||||
@singleton()
|
||||
export class BehaviorTreeService implements IService {
|
||||
export class BehaviorTreeService implements IBehaviorTreeService {
|
||||
async createNew(): Promise<void> {
|
||||
useBehaviorTreeDataStore.getState().reset();
|
||||
}
|
||||
|
||||
66
packages/behavior-tree-editor/src/tokens.ts
Normal file
66
packages/behavior-tree-editor/src/tokens.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Behavior Tree Editor Service Tokens
|
||||
* 行为树编辑器服务令牌
|
||||
*
|
||||
* 遵循"谁定义接口,谁导出 Token"的原则。
|
||||
* Following the "who defines interface, who exports token" principle.
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/engine-core';
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTree } from './domain/models/BehaviorTree';
|
||||
|
||||
/**
|
||||
* 行为树服务接口
|
||||
* Behavior Tree Service Interface
|
||||
*/
|
||||
export interface IBehaviorTreeService extends IService {
|
||||
/**
|
||||
* 创建新的行为树
|
||||
* Create a new behavior tree
|
||||
*/
|
||||
createNew(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 从文件加载行为树
|
||||
* Load behavior tree from file
|
||||
* @param filePath 文件路径 | File path
|
||||
*/
|
||||
loadFromFile(filePath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 保存行为树到文件
|
||||
* Save behavior tree to file
|
||||
* @param filePath 文件路径 | File path
|
||||
* @param metadata 可选的元数据 | Optional metadata
|
||||
*/
|
||||
saveToFile(filePath: string, metadata?: { name: string; description: string }): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取当前行为树
|
||||
* Get current behavior tree
|
||||
*/
|
||||
getCurrentTree(): BehaviorTree;
|
||||
|
||||
/**
|
||||
* 设置行为树
|
||||
* Set behavior tree
|
||||
* @param tree 行为树 | Behavior tree
|
||||
*/
|
||||
setTree(tree: BehaviorTree): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树服务令牌
|
||||
* Behavior Tree Service Token
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { BehaviorTreeServiceToken } from '@esengine/behavior-tree-editor';
|
||||
* import { PluginAPI } from '@esengine/editor-runtime';
|
||||
*
|
||||
* const service = PluginAPI.resolve(BehaviorTreeServiceToken);
|
||||
* await service.loadFromFile('/path/to/tree.btree');
|
||||
* ```
|
||||
*/
|
||||
export const BehaviorTreeServiceToken = createServiceToken<IBehaviorTreeService>('behaviorTreeService');
|
||||
@@ -130,14 +130,21 @@ export class BehaviorTreeExecutor {
|
||||
): BehaviorTreeData {
|
||||
const rootNode = nodes.find((n) => n.id === rootNodeId);
|
||||
if (!rootNode) {
|
||||
throw new Error('未找到根节点');
|
||||
throw new Error('Root node not found');
|
||||
}
|
||||
|
||||
// 如果根节点是编辑器特有的"根节点",跳过它,使用其子节点
|
||||
// If root node is the editor-specific "Root" node, skip it and use its child
|
||||
// 如果根节点是编辑器特有的"Root"节点,跳过它,使用其子节点
|
||||
let actualRootId = rootNodeId;
|
||||
let skipRootNode = false;
|
||||
|
||||
if (rootNode.template.displayName === '根节点') {
|
||||
// Check by className (preferred) or displayName (fallback for old data)
|
||||
// 通过 className(首选)或 displayName(旧数据回退)检查
|
||||
const isEditorRootNode = rootNode.template.className === 'Root' ||
|
||||
rootNode.template.displayName === '根节点' ||
|
||||
rootNode.template.displayName === 'Root';
|
||||
|
||||
if (isEditorRootNode) {
|
||||
skipRootNode = true;
|
||||
|
||||
if (rootNode.children.length === 0) {
|
||||
|
||||
@@ -92,16 +92,11 @@ function generateId(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全获取或创建 metadata
|
||||
* Safely get or create metadata
|
||||
* 更新 metadata 的修改时间
|
||||
* Update metadata modification time
|
||||
*/
|
||||
function getUpdatedMetadata(blueprint: BlueprintAsset): BlueprintAsset['metadata'] {
|
||||
const existing = blueprint.metadata || {
|
||||
name: (blueprint as any).name || 'Blueprint',
|
||||
createdAt: Date.now(),
|
||||
modifiedAt: Date.now()
|
||||
};
|
||||
return { ...existing, modifiedAt: Date.now() };
|
||||
return { ...blueprint.metadata, modifiedAt: Date.now() };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ComponentDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) {
|
||||
return {
|
||||
componentTypes: 0,
|
||||
@@ -98,7 +98,7 @@ export class ComponentDataCollector {
|
||||
|
||||
if (!scene) return 64;
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return 64;
|
||||
|
||||
let calculatedSize = 64;
|
||||
@@ -174,7 +174,7 @@ export class ComponentDataCollector {
|
||||
public calculateDetailedComponentMemory(typeName: string, scene?: IScene | null): number {
|
||||
if (!scene) return this.getEstimatedComponentSize(typeName, scene);
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return this.getEstimatedComponentSize(typeName, scene);
|
||||
|
||||
try {
|
||||
|
||||
@@ -8,18 +8,25 @@ import { HierarchySystem } from '../../ECS/Systems/HierarchySystem';
|
||||
|
||||
/**
|
||||
* 实体数据收集器
|
||||
* Entity data collector
|
||||
*
|
||||
* 收集实体的调试信息,通过公共接口访问数据。
|
||||
* Collects entity debug information through public interfaces.
|
||||
*/
|
||||
export class EntityDataCollector {
|
||||
/**
|
||||
* 收集实体数据
|
||||
* @param scene 场景实例
|
||||
* Collect entity data
|
||||
*
|
||||
* @param scene 场景实例 | Scene instance
|
||||
*/
|
||||
public collectEntityData(scene?: IScene | null): IEntityDebugData {
|
||||
if (!scene) {
|
||||
return this.getEmptyEntityDebugData();
|
||||
}
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
// 使用公共接口 | Use public interface
|
||||
const entityList = scene.entities;
|
||||
if (!entityList) {
|
||||
return this.getEmptyEntityDebugData();
|
||||
}
|
||||
@@ -56,7 +63,9 @@ export class EntityDataCollector {
|
||||
|
||||
/**
|
||||
* 获取原始实体列表
|
||||
* @param scene 场景实例
|
||||
* Get raw entity list
|
||||
*
|
||||
* @param scene 场景实例 | Scene instance
|
||||
*/
|
||||
public getRawEntityList(scene?: IScene | null): Array<{
|
||||
id: number;
|
||||
@@ -74,7 +83,8 @@ export class EntityDataCollector {
|
||||
}> {
|
||||
if (!scene) return [];
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
// 使用公共接口 | Use public interface
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return [];
|
||||
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
@@ -110,15 +120,21 @@ export class EntityDataCollector {
|
||||
try {
|
||||
if (!scene) return null;
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return null;
|
||||
|
||||
const entity = entityList.buffer.find((e: any) => e.id === entityId);
|
||||
if (!entity) return null;
|
||||
|
||||
// 使用 HierarchySystem 获取父实体
|
||||
// Use HierarchySystem to get parent entity
|
||||
const hierarchySystem = scene.getSystem(HierarchySystem);
|
||||
const parent = hierarchySystem?.getParent(entity);
|
||||
const parentName = parent?.name ?? null;
|
||||
|
||||
const baseDebugInfo = entity.getDebugInfo
|
||||
? entity.getDebugInfo()
|
||||
: this.buildFallbackEntityInfo(entity, scene);
|
||||
: this.buildFallbackEntityInfo(entity, scene, hierarchySystem);
|
||||
|
||||
const componentDetails = this.extractComponentDetails(entity.components);
|
||||
|
||||
@@ -129,7 +145,7 @@ export class EntityDataCollector {
|
||||
scene: sceneInfo.name,
|
||||
sceneName: sceneInfo.name,
|
||||
sceneType: sceneInfo.type,
|
||||
parentName: entity.parent?.name || null,
|
||||
parentName,
|
||||
components: componentDetails || [],
|
||||
componentCount: entity.components?.length || 0,
|
||||
componentTypes: entity.components?.map((comp: any) => getComponentInstanceTypeName(comp)) || []
|
||||
@@ -180,7 +196,7 @@ export class EntityDataCollector {
|
||||
return this.getEmptyEntityDebugData();
|
||||
}
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList) {
|
||||
return this.getEmptyEntityDebugData();
|
||||
}
|
||||
@@ -769,13 +785,14 @@ export class EntityDataCollector {
|
||||
try {
|
||||
if (!scene) return {};
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return {};
|
||||
|
||||
const entity = entityList.buffer.find((e: any) => e.id === entityId);
|
||||
if (!entity || componentIndex >= entity.components.length) return {};
|
||||
|
||||
const component = entity.components[componentIndex];
|
||||
if (!component) return {};
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
const propertyKeys = Object.keys(component);
|
||||
@@ -970,7 +987,7 @@ export class EntityDataCollector {
|
||||
try {
|
||||
if (!scene) return null;
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityList = scene.entities;
|
||||
if (!entityList?.buffer) return null;
|
||||
|
||||
// 找到对应的实体
|
||||
|
||||
@@ -3,13 +3,19 @@ import { IScene } from '../../ECS/IScene';
|
||||
|
||||
/**
|
||||
* 场景数据收集器
|
||||
* Scene data collector
|
||||
*
|
||||
* 收集场景的调试信息,通过公共接口访问数据。
|
||||
* Collects scene debug information through public interfaces.
|
||||
*/
|
||||
export class SceneDataCollector {
|
||||
private sceneStartTime: number = Date.now();
|
||||
|
||||
/**
|
||||
* 收集场景数据
|
||||
* @param scene 场景实例
|
||||
* Collect scene data
|
||||
*
|
||||
* @param scene 场景实例 | Scene instance
|
||||
*/
|
||||
public collectSceneData(scene?: IScene | null): ISceneDebugData {
|
||||
if (!scene) {
|
||||
@@ -26,15 +32,15 @@ export class SceneDataCollector {
|
||||
const currentTime = Date.now();
|
||||
const runTime = (currentTime - this.sceneStartTime) / 1000;
|
||||
|
||||
const entityList = (scene as any).entities;
|
||||
const entityProcessors = (scene as any).entityProcessors;
|
||||
// 使用公共接口获取数据 | Use public interface to get data
|
||||
const stats = scene.getStats();
|
||||
|
||||
return {
|
||||
currentSceneName: (scene as any).name || 'Unnamed Scene',
|
||||
isInitialized: (scene as any)._didSceneBegin || false,
|
||||
currentSceneName: scene.name || 'Unnamed Scene',
|
||||
isInitialized: true, // 如果 scene 存在,则认为已初始化 | If scene exists, consider initialized
|
||||
sceneRunTime: runTime,
|
||||
sceneEntityCount: entityList?.buffer?.length || 0,
|
||||
sceneSystemCount: entityProcessors?.processors?.length || 0,
|
||||
sceneEntityCount: stats.entityCount,
|
||||
sceneSystemCount: stats.processorCount,
|
||||
sceneUptime: runTime
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,12 +4,18 @@ import { IScene } from '../../ECS/IScene';
|
||||
|
||||
/**
|
||||
* 系统数据收集器
|
||||
* System data collector
|
||||
*
|
||||
* 收集系统的调试信息,通过公共接口访问数据。
|
||||
* Collects system debug information through public interfaces.
|
||||
*/
|
||||
export class SystemDataCollector {
|
||||
/**
|
||||
* 收集系统数据
|
||||
* @param performanceMonitor 性能监视器实例
|
||||
* @param scene 场景实例
|
||||
* Collect system data
|
||||
*
|
||||
* @param performanceMonitor 性能监视器实例 | Performance monitor instance
|
||||
* @param scene 场景实例 | Scene instance
|
||||
*/
|
||||
public collectSystemData(performanceMonitor: any, scene?: IScene | null): ISystemDebugData {
|
||||
if (!scene) {
|
||||
@@ -19,15 +25,8 @@ export class SystemDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
const entityProcessors = (scene as any).entityProcessors;
|
||||
if (!entityProcessors) {
|
||||
return {
|
||||
totalSystems: 0,
|
||||
systemsInfo: []
|
||||
};
|
||||
}
|
||||
|
||||
const systems = entityProcessors.processors || [];
|
||||
// 使用公共接口 | Use public interface
|
||||
const systems = scene.systems || [];
|
||||
|
||||
// 获取性能监控数据
|
||||
let systemStats: Map<string, any> = new Map();
|
||||
|
||||
@@ -58,7 +58,7 @@ import { EngineService } from './services/EngineService';
|
||||
import { CompilerConfigDialog } from './components/CompilerConfigDialog';
|
||||
import { checkForUpdatesOnStartup } from './utils/updater';
|
||||
import { useLocale } from './hooks/useLocale';
|
||||
import { en, zh } from './locales';
|
||||
import { en, zh, es } from './locales';
|
||||
import type { Locale } from '@esengine/editor-core';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import './styles/App.css';
|
||||
@@ -68,6 +68,7 @@ const coreInstance = Core.create({ debug: true });
|
||||
const localeService = new LocaleService();
|
||||
localeService.registerTranslations('en', en);
|
||||
localeService.registerTranslations('zh', zh);
|
||||
localeService.registerTranslations('es', es);
|
||||
Core.services.registerInstance(LocaleService, localeService);
|
||||
|
||||
Core.services.registerSingleton(GlobalBlackboardService);
|
||||
@@ -161,10 +162,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.saveScene();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
showToast(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`, 'success');
|
||||
showToast(t('scene.savedSuccess', { name: sceneState.sceneName }), 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene:', error);
|
||||
showToast(locale === 'zh' ? '保存场景失败' : 'Failed to save scene', 'error');
|
||||
showToast(t('scene.saveFailed'), 'error');
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -366,7 +367,7 @@ function App() {
|
||||
const handleOpenRecentProject = async (projectPath: string) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 1/3: 打开项目配置...' : 'Step 1/3: Opening project config...');
|
||||
setLoadingMessage(t('loading.step1'));
|
||||
|
||||
const projectService = Core.services.resolve(ProjectService);
|
||||
|
||||
@@ -400,13 +401,13 @@ function App() {
|
||||
setProjectLoaded(true);
|
||||
|
||||
// 等待引擎初始化完成(Viewport 渲染后会触发引擎初始化)
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 2/3: 初始化引擎和模块...' : 'Step 2/3: Initializing engine and modules...');
|
||||
setLoadingMessage(t('loading.step2'));
|
||||
const engineService = EngineService.getInstance();
|
||||
|
||||
// 等待引擎初始化(最多等待 30 秒,因为需要等待 Viewport 渲染)
|
||||
const engineReady = await engineService.waitForInitialization(30000);
|
||||
if (!engineReady) {
|
||||
throw new Error(locale === 'zh' ? '引擎初始化超时' : 'Engine initialization timeout');
|
||||
throw new Error(t('loading.engineTimeoutError'));
|
||||
}
|
||||
|
||||
// 加载项目插件配置并激活插件(在引擎初始化后、模块系统初始化前)
|
||||
@@ -432,7 +433,7 @@ function App() {
|
||||
|
||||
setStatus(t('header.status.projectOpened'));
|
||||
|
||||
setLoadingMessage(locale === 'zh' ? '步骤 3/3: 初始化场景...' : 'Step 3/3: Initializing scene...');
|
||||
setLoadingMessage(t('loading.step3'));
|
||||
|
||||
const sceneManagerService = Core.services.resolve(SceneManagerService);
|
||||
if (sceneManagerService) {
|
||||
@@ -440,7 +441,7 @@ function App() {
|
||||
}
|
||||
|
||||
if (pluginManager) {
|
||||
setLoadingMessage(locale === 'zh' ? '加载项目插件...' : 'Loading project plugins...');
|
||||
setLoadingMessage(t('loading.loadingPlugins'));
|
||||
await pluginLoader.loadProjectPlugins(projectPath, pluginManager);
|
||||
}
|
||||
|
||||
@@ -452,10 +453,8 @@ function App() {
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '打开项目失败' : 'Failed to Open Project',
|
||||
message: locale === 'zh'
|
||||
? `无法打开项目:\n${errorMessage}`
|
||||
: `Failed to open project:\n${errorMessage}`
|
||||
title: t('project.openFailed'),
|
||||
message: `${t('project.openFailed')}:\n${errorMessage}`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -482,22 +481,22 @@ function App() {
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(locale === 'zh' ? '正在创建项目...' : 'Creating project...');
|
||||
setLoadingMessage(t('project.creating'));
|
||||
|
||||
const projectService = Core.services.resolve(ProjectService);
|
||||
if (!projectService) {
|
||||
console.error('ProjectService not available');
|
||||
setIsLoading(false);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '创建项目失败' : 'Failed to Create Project',
|
||||
message: locale === 'zh' ? '项目服务不可用,请重启编辑器' : 'Project service is not available. Please restart the editor.'
|
||||
title: t('project.createFailed'),
|
||||
message: t('project.serviceUnavailable')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await projectService.createProject(fullProjectPath);
|
||||
|
||||
setLoadingMessage(locale === 'zh' ? '项目创建成功,正在打开...' : 'Project created, opening...');
|
||||
setLoadingMessage(t('project.createdOpening'));
|
||||
|
||||
await handleOpenRecentProject(fullProjectPath);
|
||||
} catch (error) {
|
||||
@@ -508,35 +507,29 @@ function App() {
|
||||
|
||||
if (errorMessage.includes('already exists')) {
|
||||
setConfirmDialog({
|
||||
title: locale === 'zh' ? '项目已存在' : 'Project Already Exists',
|
||||
message: locale === 'zh'
|
||||
? '该目录下已存在 ECS 项目,是否要打开该项目?'
|
||||
: 'An ECS project already exists in this directory. Do you want to open it?',
|
||||
confirmText: locale === 'zh' ? '打开项目' : 'Open Project',
|
||||
cancelText: locale === 'zh' ? '取消' : 'Cancel',
|
||||
title: t('project.alreadyExists'),
|
||||
message: t('project.existsQuestion'),
|
||||
confirmText: t('project.open'),
|
||||
cancelText: t('common.cancel'),
|
||||
onConfirm: () => {
|
||||
setConfirmDialog(null);
|
||||
setIsLoading(true);
|
||||
setLoadingMessage(locale === 'zh' ? '正在打开项目...' : 'Opening project...');
|
||||
setLoadingMessage(t('project.opening'));
|
||||
handleOpenRecentProject(fullProjectPath).catch((err) => {
|
||||
console.error('Failed to open project:', err);
|
||||
setIsLoading(false);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '打开项目失败' : 'Failed to Open Project',
|
||||
message: locale === 'zh'
|
||||
? `无法打开项目:\n${err instanceof Error ? err.message : String(err)}`
|
||||
: `Failed to open project:\n${err instanceof Error ? err.message : String(err)}`
|
||||
title: t('project.openFailed'),
|
||||
message: `${t('project.openFailed')}:\n${err instanceof Error ? err.message : String(err)}`
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setStatus(locale === 'zh' ? '创建项目失败' : 'Failed to create project');
|
||||
setStatus(t('project.createFailed'));
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '创建项目失败' : 'Failed to Create Project',
|
||||
message: locale === 'zh'
|
||||
? `无法创建项目:\n${errorMessage}`
|
||||
: `Failed to create project:\n${errorMessage}`
|
||||
title: t('project.createFailed'),
|
||||
message: `${t('project.createFailed')}:\n${errorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -560,10 +553,10 @@ function App() {
|
||||
|
||||
try {
|
||||
await sceneManager.newScene();
|
||||
setStatus(locale === 'zh' ? '已创建新场景' : 'New scene created');
|
||||
setStatus(t('scene.newCreated'));
|
||||
} catch (error) {
|
||||
console.error('Failed to create new scene:', error);
|
||||
setStatus(locale === 'zh' ? '创建场景失败' : 'Failed to create scene');
|
||||
setStatus(t('scene.createFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -576,10 +569,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.openScene();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已打开场景: ${sceneState.sceneName}` : `Scene opened: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to open scene:', error);
|
||||
setStatus(locale === 'zh' ? '打开场景失败' : 'Failed to open scene');
|
||||
setStatus(t('scene.openFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -592,15 +585,13 @@ function App() {
|
||||
try {
|
||||
await sceneManager.openScene(scenePath);
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已打开场景: ${sceneState.sceneName}` : `Scene opened: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to open scene:', error);
|
||||
setStatus(locale === 'zh' ? '打开场景失败' : 'Failed to open scene');
|
||||
setStatus(t('scene.openFailed'));
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '打开场景失败' : 'Failed to Open Scene',
|
||||
message: locale === 'zh'
|
||||
? `无法打开场景:\n${error instanceof Error ? error.message : String(error)}`
|
||||
: `Failed to open scene:\n${error instanceof Error ? error.message : String(error)}`
|
||||
title: t('scene.openFailed'),
|
||||
message: `${t('scene.openFailed')}:\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}, [sceneManager, locale]);
|
||||
@@ -615,10 +606,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.saveScene();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene:', error);
|
||||
setStatus(locale === 'zh' ? '保存场景失败' : 'Failed to save scene');
|
||||
setStatus(t('scene.saveFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -631,10 +622,10 @@ function App() {
|
||||
try {
|
||||
await sceneManager.saveSceneAs();
|
||||
const sceneState = sceneManager.getSceneState();
|
||||
setStatus(locale === 'zh' ? `已保存场景: ${sceneState.sceneName}` : `Scene saved: ${sceneState.sceneName}`);
|
||||
setStatus(t('scene.savedSuccess', { name: sceneState.sceneName }));
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene as:', error);
|
||||
setStatus(locale === 'zh' ? '另存场景失败' : 'Failed to save scene as');
|
||||
setStatus(t('scene.saveAsFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -726,10 +717,10 @@ function App() {
|
||||
// 7. 触发面板重新渲染
|
||||
setPluginUpdateTrigger((prev) => prev + 1);
|
||||
|
||||
showToast(locale === 'zh' ? '插件已重新加载' : 'Plugins reloaded', 'success');
|
||||
showToast(t('plugin.reloadedSuccess'), 'success');
|
||||
} catch (error) {
|
||||
console.error('Failed to reload plugins:', error);
|
||||
showToast(locale === 'zh' ? '重新加载插件失败' : 'Failed to reload plugins', 'error');
|
||||
showToast(t('plugin.reloadFailed'), 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -739,25 +730,25 @@ function App() {
|
||||
const corePanels: FlexDockPanel[] = [
|
||||
{
|
||||
id: 'scene-hierarchy',
|
||||
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
||||
title: t('panel.sceneHierarchy'),
|
||||
content: <SceneHierarchy entityStore={entityStore} messageHub={messageHub} commandManager={commandManager} />,
|
||||
closable: false
|
||||
},
|
||||
{
|
||||
id: 'viewport',
|
||||
title: locale === 'zh' ? '视口' : 'Viewport',
|
||||
title: t('panel.viewport'),
|
||||
content: <Viewport locale={locale} messageHub={messageHub} />,
|
||||
closable: false
|
||||
},
|
||||
{
|
||||
id: 'inspector',
|
||||
title: locale === 'zh' ? '检视器' : 'Inspector',
|
||||
title: t('panel.inspector'),
|
||||
content: <Inspector entityStore={entityStore} messageHub={messageHub} inspectorRegistry={inspectorRegistry!} projectPath={currentProjectPath} commandManager={commandManager} />,
|
||||
closable: false
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
title: locale === 'zh' ? '社区论坛' : 'Forum',
|
||||
title: t('panel.forum'),
|
||||
content: <ForumPanel />,
|
||||
closable: true
|
||||
}
|
||||
@@ -776,9 +767,12 @@ function App() {
|
||||
})
|
||||
.map((panelDesc) => {
|
||||
const Component = panelDesc.component;
|
||||
// 使用 titleKey 翻译,回退到 title
|
||||
// Use titleKey for translation, fallback to title
|
||||
const title = panelDesc.titleKey ? t(panelDesc.titleKey) : panelDesc.title;
|
||||
return {
|
||||
id: panelDesc.id,
|
||||
title: panelDesc.titleZh && locale === 'zh' ? panelDesc.titleZh : panelDesc.title,
|
||||
title,
|
||||
content: <Component key={`${panelDesc.id}-${pluginUpdateTrigger}`} projectPath={currentProjectPath} />,
|
||||
closable: panelDesc.closable ?? true
|
||||
};
|
||||
@@ -793,8 +787,10 @@ function App() {
|
||||
.map((panelId) => {
|
||||
const panelDesc = uiRegistry.getPanel(panelId)!;
|
||||
// 优先使用动态标题,否则使用默认标题
|
||||
// Prefer dynamic title, fallback to default title
|
||||
const customTitle = dynamicPanelTitles.get(panelId);
|
||||
const defaultTitle = panelDesc.titleZh && locale === 'zh' ? panelDesc.titleZh : panelDesc.title;
|
||||
// 使用 titleKey 翻译,回退到 title
|
||||
const defaultTitle = panelDesc.titleKey ? t(panelDesc.titleKey) : panelDesc.title;
|
||||
|
||||
// 支持 component 或 render 两种方式
|
||||
let content: React.ReactNode;
|
||||
@@ -855,16 +851,13 @@ function App() {
|
||||
} catch (error) {
|
||||
console.error('[App] Failed to delete project:', error);
|
||||
setErrorDialog({
|
||||
title: locale === 'zh' ? '删除项目失败' : 'Failed to Delete Project',
|
||||
message: locale === 'zh'
|
||||
? `无法删除项目:\n${error instanceof Error ? error.message : String(error)}`
|
||||
: `Failed to delete project:\n${error instanceof Error ? error.message : String(error)}`
|
||||
title: t('project.deleteFailed'),
|
||||
message: `${t('project.deleteFailed')}:\n${error instanceof Error ? error.message : String(error)}`
|
||||
});
|
||||
}
|
||||
}}
|
||||
onLocaleChange={handleLocaleChange}
|
||||
recentProjects={recentProjects}
|
||||
locale={locale}
|
||||
/>
|
||||
<ProjectCreationWizard
|
||||
isOpen={showProjectWizard}
|
||||
@@ -918,7 +911,6 @@ function App() {
|
||||
<>
|
||||
<TitleBar
|
||||
projectName={projectName}
|
||||
locale={locale}
|
||||
uiRegistry={uiRegistry || undefined}
|
||||
messageHub={messageHub || undefined}
|
||||
pluginManager={pluginManager || undefined}
|
||||
@@ -943,7 +935,6 @@ function App() {
|
||||
onOpenBuildSettings={() => setShowBuildSettings(true)}
|
||||
/>
|
||||
<MainToolbar
|
||||
locale={locale}
|
||||
messageHub={messageHub || undefined}
|
||||
commandManager={commandManager}
|
||||
onSaveScene={handleSaveScene}
|
||||
@@ -1013,14 +1004,13 @@ function App() {
|
||||
)}
|
||||
|
||||
{showAbout && (
|
||||
<AboutDialog onClose={() => setShowAbout(false)} locale={locale} />
|
||||
<AboutDialog onClose={() => setShowAbout(false)} />
|
||||
)}
|
||||
|
||||
{showPluginGenerator && (
|
||||
<PluginGeneratorWindow
|
||||
onClose={() => setShowPluginGenerator(false)}
|
||||
projectPath={currentProjectPath}
|
||||
locale={locale}
|
||||
onSuccess={async () => {
|
||||
if (currentProjectPath && pluginManager) {
|
||||
await pluginLoader.loadProjectPlugins(currentProjectPath, pluginManager);
|
||||
@@ -1033,7 +1023,6 @@ function App() {
|
||||
<BuildSettingsWindow
|
||||
onClose={() => setShowBuildSettings(false)}
|
||||
projectPath={currentProjectPath || undefined}
|
||||
locale={locale}
|
||||
buildService={buildService || undefined}
|
||||
sceneManager={sceneManager || undefined}
|
||||
/>
|
||||
|
||||
@@ -4,14 +4,15 @@ import { checkForUpdates, installUpdate } from '../utils/updater';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { MiniParticleLogo } from './MiniParticleLogo';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/AboutDialog.css';
|
||||
|
||||
interface AboutDialogProps {
|
||||
onClose: () => void;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
export function AboutDialog({ onClose }: AboutDialogProps) {
|
||||
const { t } = useLocale();
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'latest' | 'error' | 'installing'>('idle');
|
||||
@@ -31,44 +32,6 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
fetchVersion();
|
||||
}, []);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
title: 'About ESEngine Editor',
|
||||
version: 'Version',
|
||||
description: 'High-performance game editor for ECS-based game development',
|
||||
checkUpdate: 'Check for Updates',
|
||||
checking: 'Checking...',
|
||||
updateAvailable: 'New version available',
|
||||
latest: 'You are using the latest version',
|
||||
error: 'Failed to check for updates',
|
||||
download: 'Download & Install',
|
||||
installing: 'Installing...',
|
||||
close: 'Close',
|
||||
copyright: '© 2025 ESEngine. All rights reserved.',
|
||||
website: 'Website',
|
||||
github: 'GitHub'
|
||||
},
|
||||
zh: {
|
||||
title: '关于 ESEngine Editor',
|
||||
version: '版本',
|
||||
description: '高性能游戏编辑器,基于 ECS 架构',
|
||||
checkUpdate: '检查更新',
|
||||
checking: '检查中...',
|
||||
updateAvailable: '发现新版本',
|
||||
latest: '您正在使用最新版本',
|
||||
error: '检查更新失败',
|
||||
download: '下载并安装',
|
||||
installing: '正在安装...',
|
||||
close: '关闭',
|
||||
copyright: '© 2025 ESEngine. 保留所有权利。',
|
||||
website: '官网',
|
||||
github: 'GitHub'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
const handleCheckUpdate = async () => {
|
||||
setChecking(true);
|
||||
setUpdateStatus('checking');
|
||||
@@ -136,15 +99,15 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
const getStatusText = () => {
|
||||
switch (updateStatus) {
|
||||
case 'checking':
|
||||
return t('checking');
|
||||
return t('about.checking');
|
||||
case 'available':
|
||||
return `${t('updateAvailable')} (v${newVersion})`;
|
||||
return `${t('about.updateAvailable')} (v${newVersion})`;
|
||||
case 'installing':
|
||||
return t('installing');
|
||||
return t('about.installing');
|
||||
case 'latest':
|
||||
return t('latest');
|
||||
return t('about.latest');
|
||||
case 'error':
|
||||
return t('error');
|
||||
return t('about.error');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -162,7 +125,7 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div className="about-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="about-header">
|
||||
<h2>{t('title')}</h2>
|
||||
<h2>{t('about.title')}</h2>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -176,10 +139,10 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
<div className="about-info">
|
||||
<h3>ESEngine Editor</h3>
|
||||
<p className="about-version">
|
||||
{t('version')}: Editor {version}
|
||||
{t('about.version')}: Editor {version}
|
||||
</p>
|
||||
<p className="about-description">
|
||||
{t('description')}
|
||||
{t('about.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -192,12 +155,12 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
{checking ? (
|
||||
<>
|
||||
<RefreshCw size={16} className="animate-spin" />
|
||||
<span>{t('checking')}</span>
|
||||
<span>{t('about.checking')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw size={16} />
|
||||
<span>{t('checkUpdate')}</span>
|
||||
<span>{t('about.checkUpdate')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -218,12 +181,12 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
{installing ? (
|
||||
<>
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>{t('installing')}</span>
|
||||
<span>{t('about.installing')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download size={16} />
|
||||
<span>{t('download')}</span>
|
||||
<span>{t('about.download')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -239,18 +202,18 @@ export function AboutDialog({ onClose, locale = 'en' }: AboutDialogProps) {
|
||||
}}
|
||||
className="about-link"
|
||||
>
|
||||
{t('github')}
|
||||
{t('about.github')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="about-footer">
|
||||
<p>{t('copyright')}</p>
|
||||
<p>{t('about.copyright')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="about-actions">
|
||||
<button className="btn-primary" onClick={onClose}>
|
||||
{t('close')}
|
||||
{t('about.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/AssetPickerDialog.css';
|
||||
|
||||
interface AssetPickerDialogProps {
|
||||
@@ -25,6 +26,8 @@ interface AssetItem {
|
||||
type ViewMode = 'list' | 'grid';
|
||||
|
||||
export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) {
|
||||
const { t, locale: currentLocale } = useLocale();
|
||||
|
||||
// 计算实际的资产目录路径
|
||||
const actualAssetPath = assetBasePath
|
||||
? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/')
|
||||
@@ -37,33 +40,6 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list');
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'Select Asset',
|
||||
loading: 'Loading...',
|
||||
empty: 'No assets found',
|
||||
select: 'Select',
|
||||
cancel: 'Cancel',
|
||||
search: 'Search...',
|
||||
back: 'Back',
|
||||
listView: 'List View',
|
||||
gridView: 'Grid View'
|
||||
},
|
||||
zh: {
|
||||
title: '选择资产',
|
||||
loading: '加载中...',
|
||||
empty: '没有找到资产',
|
||||
select: '选择',
|
||||
cancel: '取消',
|
||||
search: '搜索...',
|
||||
back: '返回上级',
|
||||
listView: '列表视图',
|
||||
gridView: '网格视图'
|
||||
}
|
||||
};
|
||||
|
||||
const t = translations[locale as keyof typeof translations] || translations.en;
|
||||
|
||||
useEffect(() => {
|
||||
loadAssets(currentPath);
|
||||
}, [currentPath]);
|
||||
@@ -118,7 +94,8 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
const formatDate = (timestamp?: number): string => {
|
||||
if (!timestamp) return '';
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', {
|
||||
const localeMap: Record<string, string> = { zh: 'zh-CN', en: 'en-US', es: 'es-ES' };
|
||||
return date.toLocaleDateString(localeMap[currentLocale] || 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
@@ -213,7 +190,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
<div className="asset-picker-overlay" onClick={onClose}>
|
||||
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="asset-picker-header">
|
||||
<h3>{t.title}</h3>
|
||||
<h3>{t('assetPicker.title')}</h3>
|
||||
<button className="asset-picker-close" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -224,7 +201,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
className="toolbar-button"
|
||||
onClick={handleGoBack}
|
||||
disabled={!canGoBack}
|
||||
title={t.back}
|
||||
title={t('assetPicker.back')}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
@@ -247,14 +224,14 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
<button
|
||||
className={`toolbar-button ${viewMode === 'list' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('list')}
|
||||
title={t.listView}
|
||||
title={t('assetPicker.listView')}
|
||||
>
|
||||
<List size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={`toolbar-button ${viewMode === 'grid' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('grid')}
|
||||
title={t.gridView}
|
||||
title={t('assetPicker.gridView')}
|
||||
>
|
||||
<Grid size={16} />
|
||||
</button>
|
||||
@@ -265,7 +242,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
<Search size={16} className="search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t.search}
|
||||
placeholder={t('assetPicker.search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="search-input"
|
||||
@@ -282,9 +259,9 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
|
||||
<div className="asset-picker-content">
|
||||
{loading ? (
|
||||
<div className="asset-picker-loading">{t.loading}</div>
|
||||
<div className="asset-picker-loading">{t('assetPicker.loading')}</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="asset-picker-empty">{t.empty}</div>
|
||||
<div className="asset-picker-empty">{t('assetPicker.empty')}</div>
|
||||
) : (
|
||||
<div className={`asset-picker-list ${viewMode}`}>
|
||||
{filteredAssets.map((item, index) => (
|
||||
@@ -318,18 +295,18 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
|
||||
<div className="asset-picker-footer">
|
||||
<div className="footer-info">
|
||||
{filteredAssets.length} {locale === 'zh' ? '项' : 'items'}
|
||||
{t('assetPicker.itemCount', { count: filteredAssets.length })}
|
||||
</div>
|
||||
<div className="footer-buttons">
|
||||
<button className="asset-picker-cancel" onClick={onClose}>
|
||||
{t.cancel}
|
||||
{t('assetPicker.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="asset-picker-select"
|
||||
onClick={handleSelect}
|
||||
disabled={!selectedPath}
|
||||
>
|
||||
{t.select}
|
||||
{t('assetPicker.select')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { BuildService, BuildProgress, BuildConfig, WebBuildConfig, WeChatBuildConfig, SceneManagerService } from '@esengine/editor-core';
|
||||
import { BuildPlatform, BuildStatus } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/BuildSettingsPanel.css';
|
||||
|
||||
// ==================== Types | 类型定义 ====================
|
||||
@@ -89,110 +90,25 @@ const DEFAULT_SETTINGS: BuildSettings = {
|
||||
bundleModules: false,
|
||||
};
|
||||
|
||||
// ==================== i18n | 国际化 ====================
|
||||
// ==================== Status Key Mapping | 状态键映射 ====================
|
||||
|
||||
const i18n = {
|
||||
en: {
|
||||
buildProfiles: 'Build Profiles',
|
||||
addBuildProfile: 'Add Build Profile',
|
||||
playerSettings: 'Player Settings',
|
||||
assetImportOverrides: 'Asset Import Overrides',
|
||||
platforms: 'Platforms',
|
||||
sceneList: 'Scene List',
|
||||
active: 'Active',
|
||||
switchProfile: 'Switch Profile',
|
||||
build: 'Build',
|
||||
buildAndRun: 'Build And Run',
|
||||
buildData: 'Build Data',
|
||||
scriptingDefines: 'Scripting Defines',
|
||||
listIsEmpty: 'List is empty',
|
||||
addOpenScenes: 'Add Open Scenes',
|
||||
platformSettings: 'Platform Settings',
|
||||
architecture: 'Architecture',
|
||||
developmentBuild: 'Development Build',
|
||||
sourceMap: 'Source Map',
|
||||
compressionMethod: 'Compression Method',
|
||||
bundleModules: 'Bundle Modules',
|
||||
bundleModulesHint: 'Merge all modules into single file',
|
||||
separateModulesHint: 'Keep modules as separate files',
|
||||
playerSettingsOverrides: 'Player Settings Overrides',
|
||||
companyName: 'Company Name',
|
||||
productName: 'Product Name',
|
||||
version: 'Version',
|
||||
defaultIcon: 'Default Icon',
|
||||
none: 'None',
|
||||
// Build progress | 构建进度
|
||||
buildInProgress: 'Build in Progress',
|
||||
preparing: 'Preparing...',
|
||||
compiling: 'Compiling...',
|
||||
packaging: 'Packaging assets...',
|
||||
copying: 'Copying files...',
|
||||
postProcessing: 'Post-processing...',
|
||||
completed: 'Completed',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
buildSucceeded: 'Build succeeded!',
|
||||
buildFailed: 'Build failed',
|
||||
warnings: 'Warnings',
|
||||
outputPath: 'Output Path',
|
||||
duration: 'Duration',
|
||||
},
|
||||
zh: {
|
||||
buildProfiles: '构建配置',
|
||||
addBuildProfile: '添加构建配置',
|
||||
playerSettings: '玩家设置',
|
||||
assetImportOverrides: '资源导入覆盖',
|
||||
platforms: '平台',
|
||||
sceneList: '场景列表',
|
||||
active: '激活',
|
||||
switchProfile: '切换配置',
|
||||
build: '构建',
|
||||
buildAndRun: '构建并运行',
|
||||
buildData: '构建数据',
|
||||
scriptingDefines: '脚本定义',
|
||||
listIsEmpty: '列表为空',
|
||||
addOpenScenes: '添加已打开的场景',
|
||||
platformSettings: '平台设置',
|
||||
architecture: '架构',
|
||||
developmentBuild: '开发版本',
|
||||
sourceMap: 'Source Map',
|
||||
compressionMethod: '压缩方式',
|
||||
bundleModules: '打包模块',
|
||||
bundleModulesHint: '合并所有模块为单文件',
|
||||
separateModulesHint: '保持模块为独立文件',
|
||||
playerSettingsOverrides: '玩家设置覆盖',
|
||||
companyName: '公司名称',
|
||||
productName: '产品名称',
|
||||
version: '版本',
|
||||
defaultIcon: '默认图标',
|
||||
none: '无',
|
||||
// Build progress | 构建进度
|
||||
buildInProgress: '正在构建',
|
||||
preparing: '准备中...',
|
||||
compiling: '编译中...',
|
||||
packaging: '打包资源...',
|
||||
copying: '复制文件...',
|
||||
postProcessing: '后处理...',
|
||||
completed: '已完成',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
cancel: '取消',
|
||||
close: '关闭',
|
||||
buildSucceeded: '构建成功!',
|
||||
buildFailed: '构建失败',
|
||||
warnings: '警告',
|
||||
outputPath: '输出路径',
|
||||
duration: '耗时',
|
||||
}
|
||||
/** Map BuildStatus to translation key | 将 BuildStatus 映射到翻译键 */
|
||||
const buildStatusKeys: Record<BuildStatus, string> = {
|
||||
[BuildStatus.Idle]: 'buildSettings.preparing',
|
||||
[BuildStatus.Preparing]: 'buildSettings.preparing',
|
||||
[BuildStatus.Compiling]: 'buildSettings.compiling',
|
||||
[BuildStatus.Packaging]: 'buildSettings.packaging',
|
||||
[BuildStatus.Copying]: 'buildSettings.copying',
|
||||
[BuildStatus.PostProcessing]: 'buildSettings.postProcessing',
|
||||
[BuildStatus.Completed]: 'buildSettings.completed',
|
||||
[BuildStatus.Failed]: 'buildSettings.failed',
|
||||
[BuildStatus.Cancelled]: 'buildSettings.cancelled'
|
||||
};
|
||||
|
||||
// ==================== Props | 属性 ====================
|
||||
|
||||
interface BuildSettingsPanelProps {
|
||||
projectPath?: string;
|
||||
locale?: string;
|
||||
buildService?: BuildService;
|
||||
sceneManager?: SceneManagerService;
|
||||
onBuild?: (profile: BuildProfile, settings: BuildSettings) => void;
|
||||
@@ -203,13 +119,12 @@ interface BuildSettingsPanelProps {
|
||||
|
||||
export function BuildSettingsPanel({
|
||||
projectPath,
|
||||
locale = 'en',
|
||||
buildService,
|
||||
sceneManager,
|
||||
onBuild,
|
||||
onClose
|
||||
}: BuildSettingsPanelProps) {
|
||||
const t = i18n[locale as keyof typeof i18n] || i18n.en;
|
||||
const { t } = useLocale();
|
||||
|
||||
// State | 状态
|
||||
const [profiles, setProfiles] = useState<BuildProfile[]>([
|
||||
@@ -397,18 +312,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
// Get status message | 获取状态消息
|
||||
const getStatusMessage = useCallback((status: BuildStatus): string => {
|
||||
const statusMessages: Record<BuildStatus, keyof typeof i18n.en> = {
|
||||
[BuildStatus.Idle]: 'preparing',
|
||||
[BuildStatus.Preparing]: 'preparing',
|
||||
[BuildStatus.Compiling]: 'compiling',
|
||||
[BuildStatus.Packaging]: 'packaging',
|
||||
[BuildStatus.Copying]: 'copying',
|
||||
[BuildStatus.PostProcessing]: 'postProcessing',
|
||||
[BuildStatus.Completed]: 'completed',
|
||||
[BuildStatus.Failed]: 'failed',
|
||||
[BuildStatus.Cancelled]: 'cancelled'
|
||||
};
|
||||
return t[statusMessages[status]] || status;
|
||||
return t(buildStatusKeys[status]) || status;
|
||||
}, [t]);
|
||||
|
||||
const handleAddScene = useCallback(() => {
|
||||
@@ -466,12 +370,12 @@ export function BuildSettingsPanel({
|
||||
<div className="build-settings-tabs">
|
||||
<div className="build-settings-tab active">
|
||||
<Package size={14} />
|
||||
{t.buildProfiles}
|
||||
{t('buildSettings.buildProfiles')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="build-settings-header-actions">
|
||||
<button className="build-settings-header-btn">{t.playerSettings}</button>
|
||||
<button className="build-settings-header-btn">{t.assetImportOverrides}</button>
|
||||
<button className="build-settings-header-btn">{t('buildSettings.playerSettings')}</button>
|
||||
<button className="build-settings-header-btn">{t('buildSettings.assetImportOverrides')}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -479,7 +383,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-settings-add-bar">
|
||||
<button className="build-settings-add-btn" onClick={handleAddProfile}>
|
||||
<Plus size={14} />
|
||||
{t.addBuildProfile}
|
||||
{t('buildSettings.addBuildProfile')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -489,7 +393,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-settings-sidebar">
|
||||
{/* Platforms Section | 平台部分 */}
|
||||
<div className="build-settings-section">
|
||||
<div className="build-settings-section-header">{t.platforms}</div>
|
||||
<div className="build-settings-section-header">{t('buildSettings.platforms')}</div>
|
||||
<div className="build-settings-platform-list">
|
||||
{PLATFORMS.map(platform => {
|
||||
const isActive = profiles.some(p => p.platform === platform.platform && p.isActive);
|
||||
@@ -501,7 +405,7 @@ export function BuildSettingsPanel({
|
||||
>
|
||||
<span className="build-settings-platform-icon">{platform.icon}</span>
|
||||
<span className="build-settings-platform-label">{platform.label}</span>
|
||||
{isActive && <span className="build-settings-active-badge">{t.active}</span>}
|
||||
{isActive && <span className="build-settings-active-badge">{t('buildSettings.active')}</span>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -510,7 +414,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
{/* Build Profiles Section | 构建配置部分 */}
|
||||
<div className="build-settings-section">
|
||||
<div className="build-settings-section-header">{t.buildProfiles}</div>
|
||||
<div className="build-settings-section-header">{t('buildSettings.buildProfiles')}</div>
|
||||
<div className="build-settings-profile-list">
|
||||
{profiles
|
||||
.filter(p => p.platform === selectedPlatform)
|
||||
@@ -546,9 +450,9 @@ export function BuildSettingsPanel({
|
||||
</div>
|
||||
</div>
|
||||
<div className="build-settings-details-actions">
|
||||
<button className="build-settings-btn secondary">{t.switchProfile}</button>
|
||||
<button className="build-settings-btn secondary">{t('buildSettings.switchProfile')}</button>
|
||||
<button className="build-settings-btn primary" onClick={handleBuild}>
|
||||
{t.build}
|
||||
{t('buildSettings.build')}
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -556,7 +460,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
{/* Build Data Section | 构建数据部分 */}
|
||||
<div className="build-settings-card">
|
||||
<div className="build-settings-card-header">{t.buildData}</div>
|
||||
<div className="build-settings-card-header">{t('buildSettings.buildData')}</div>
|
||||
|
||||
{/* Scene List | 场景列表 */}
|
||||
<div className="build-settings-field-group">
|
||||
@@ -565,7 +469,7 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('sceneList')}
|
||||
>
|
||||
{expandedSections.sceneList ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>{t.sceneList}</span>
|
||||
<span>{t('buildSettings.sceneList')}</span>
|
||||
</div>
|
||||
{expandedSections.sceneList && (
|
||||
<div className="build-settings-field-content">
|
||||
@@ -583,7 +487,7 @@ export function BuildSettingsPanel({
|
||||
</div>
|
||||
<div className="build-settings-field-actions">
|
||||
<button className="build-settings-btn text" onClick={handleAddScene}>
|
||||
{t.addOpenScenes}
|
||||
{t('buildSettings.addOpenScenes')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -597,13 +501,13 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('scriptingDefines')}
|
||||
>
|
||||
{expandedSections.scriptingDefines ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>{t.scriptingDefines}</span>
|
||||
<span>{t('buildSettings.scriptingDefines')}</span>
|
||||
</div>
|
||||
{expandedSections.scriptingDefines && (
|
||||
<div className="build-settings-field-content">
|
||||
<div className="build-settings-defines-list">
|
||||
{settings.scriptingDefines.length === 0 ? (
|
||||
<div className="build-settings-empty-text">{t.listIsEmpty}</div>
|
||||
<div className="build-settings-empty-text">{t('buildSettings.listIsEmpty')}</div>
|
||||
) : (
|
||||
settings.scriptingDefines.map((define, index) => (
|
||||
<div key={index} className="build-settings-define-item">
|
||||
@@ -628,7 +532,7 @@ export function BuildSettingsPanel({
|
||||
|
||||
{/* Platform Settings Section | 平台设置部分 */}
|
||||
<div className="build-settings-card">
|
||||
<div className="build-settings-card-header">{t.platformSettings}</div>
|
||||
<div className="build-settings-card-header">{t('buildSettings.platformSettings')}</div>
|
||||
|
||||
<div className="build-settings-field-group">
|
||||
<div
|
||||
@@ -636,13 +540,13 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('platformSettings')}
|
||||
>
|
||||
{expandedSections.platformSettings ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>{currentPlatformConfig?.label} Settings</span>
|
||||
<span>{currentPlatformConfig?.label} {t('buildSettings.settings')}</span>
|
||||
</div>
|
||||
{expandedSections.platformSettings && (
|
||||
<div className="build-settings-field-content">
|
||||
<div className="build-settings-form">
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.developmentBuild}</label>
|
||||
<label>{t('buildSettings.developmentBuild')}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.developmentBuild}
|
||||
@@ -653,7 +557,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.sourceMap}</label>
|
||||
<label>{t('buildSettings.sourceMap')}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.sourceMap}
|
||||
@@ -664,7 +568,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.compressionMethod}</label>
|
||||
<label>{t('buildSettings.compressionMethod')}</label>
|
||||
<select
|
||||
value={settings.compressionMethod}
|
||||
onChange={e => setSettings(prev => ({
|
||||
@@ -678,7 +582,7 @@ export function BuildSettingsPanel({
|
||||
</select>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.bundleModules}</label>
|
||||
<label>{t('buildSettings.bundleModules')}</label>
|
||||
<div className="build-settings-toggle-group">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -689,7 +593,7 @@ export function BuildSettingsPanel({
|
||||
}))}
|
||||
/>
|
||||
<span className="build-settings-hint">
|
||||
{settings.bundleModules ? t.bundleModulesHint : t.separateModulesHint}
|
||||
{settings.bundleModules ? t('buildSettings.bundleModulesHint') : t('buildSettings.separateModulesHint')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -702,7 +606,7 @@ export function BuildSettingsPanel({
|
||||
{/* Player Settings Overrides | 玩家设置覆盖 */}
|
||||
<div className="build-settings-card">
|
||||
<div className="build-settings-card-header">
|
||||
{t.playerSettingsOverrides}
|
||||
{t('buildSettings.playerSettingsOverrides')}
|
||||
<button className="build-settings-more-btn">
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
@@ -714,13 +618,13 @@ export function BuildSettingsPanel({
|
||||
onClick={() => toggleSection('playerSettings')}
|
||||
>
|
||||
{expandedSections.playerSettings ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
<span>Player Settings</span>
|
||||
<span>{t('buildSettings.playerSettings')}</span>
|
||||
</div>
|
||||
{expandedSections.playerSettings && (
|
||||
<div className="build-settings-field-content">
|
||||
<div className="build-settings-form">
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.companyName}</label>
|
||||
<label>{t('buildSettings.companyName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.companyName}
|
||||
@@ -731,7 +635,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.productName}</label>
|
||||
<label>{t('buildSettings.productName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.productName}
|
||||
@@ -742,7 +646,7 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.version}</label>
|
||||
<label>{t('buildSettings.version')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.version}
|
||||
@@ -753,9 +657,9 @@ export function BuildSettingsPanel({
|
||||
/>
|
||||
</div>
|
||||
<div className="build-settings-form-row">
|
||||
<label>{t.defaultIcon}</label>
|
||||
<label>{t('buildSettings.defaultIcon')}</label>
|
||||
<div className="build-settings-icon-picker">
|
||||
<span>{t.none}</span>
|
||||
<span>{t('buildSettings.none')}</span>
|
||||
<span className="build-settings-icon-hint">(Texture 2D)</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -767,7 +671,7 @@ export function BuildSettingsPanel({
|
||||
</>
|
||||
) : (
|
||||
<div className="build-settings-no-selection">
|
||||
<p>Select a platform or build profile</p>
|
||||
<p>{t('buildSettings.selectPlatform')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -778,7 +682,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-progress-overlay">
|
||||
<div className="build-progress-dialog">
|
||||
<div className="build-progress-header">
|
||||
<h3>{t.buildInProgress}</h3>
|
||||
<h3>{t('buildSettings.buildInProgress')}</h3>
|
||||
{!isBuilding && (
|
||||
<button
|
||||
className="build-progress-close"
|
||||
@@ -806,9 +710,9 @@ export function BuildSettingsPanel({
|
||||
{isBuilding ? (
|
||||
buildProgress?.message || getStatusMessage(buildProgress?.status || BuildStatus.Preparing)
|
||||
) : buildResult?.success ? (
|
||||
t.buildSucceeded
|
||||
t('buildSettings.buildSucceeded')
|
||||
) : (
|
||||
t.buildFailed
|
||||
t('buildSettings.buildFailed')
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -831,11 +735,11 @@ export function BuildSettingsPanel({
|
||||
{buildResult.success && (
|
||||
<>
|
||||
<div className="build-result-row">
|
||||
<span className="build-result-label">{t.outputPath}:</span>
|
||||
<span className="build-result-label">{t('buildSettings.outputPath')}:</span>
|
||||
<span className="build-result-value">{buildResult.outputPath}</span>
|
||||
</div>
|
||||
<div className="build-result-row">
|
||||
<span className="build-result-label">{t.duration}:</span>
|
||||
<span className="build-result-label">{t('buildSettings.duration')}:</span>
|
||||
<span className="build-result-value">
|
||||
{(buildResult.duration / 1000).toFixed(2)}s
|
||||
</span>
|
||||
@@ -856,7 +760,7 @@ export function BuildSettingsPanel({
|
||||
<div className="build-result-warnings">
|
||||
<div className="build-result-warnings-header">
|
||||
<AlertTriangle size={14} />
|
||||
<span>{t.warnings} ({buildResult.warnings.length})</span>
|
||||
<span>{t('buildSettings.warnings')} ({buildResult.warnings.length})</span>
|
||||
</div>
|
||||
<ul className="build-result-warnings-list">
|
||||
{buildResult.warnings.map((warning, index) => (
|
||||
@@ -876,14 +780,14 @@ export function BuildSettingsPanel({
|
||||
className="build-settings-btn secondary"
|
||||
onClick={handleCancelBuild}
|
||||
>
|
||||
{t.cancel}
|
||||
{t('buildSettings.cancel')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="build-settings-btn primary"
|
||||
onClick={handleCloseBuildProgress}
|
||||
>
|
||||
{t.close}
|
||||
{t('buildSettings.close')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
import { X } from 'lucide-react';
|
||||
import type { BuildService, SceneManagerService } from '@esengine/editor-core';
|
||||
import { BuildSettingsPanel } from './BuildSettingsPanel';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/BuildSettingsWindow.css';
|
||||
|
||||
interface BuildSettingsWindowProps {
|
||||
projectPath?: string;
|
||||
locale?: string;
|
||||
buildService?: BuildService;
|
||||
sceneManager?: SceneManagerService;
|
||||
onClose: () => void;
|
||||
@@ -21,22 +21,17 @@ interface BuildSettingsWindowProps {
|
||||
|
||||
export function BuildSettingsWindow({
|
||||
projectPath,
|
||||
locale = 'en',
|
||||
buildService,
|
||||
sceneManager,
|
||||
onClose
|
||||
}: BuildSettingsWindowProps) {
|
||||
const t = locale === 'zh' ? {
|
||||
title: '构建设置'
|
||||
} : {
|
||||
title: 'Build Settings'
|
||||
};
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="build-settings-window-overlay">
|
||||
<div className="build-settings-window">
|
||||
<div className="build-settings-window-header">
|
||||
<h2>{t.title}</h2>
|
||||
<h2>{t('build.settingsTitle')}</h2>
|
||||
<button
|
||||
className="build-settings-window-close"
|
||||
onClick={onClose}
|
||||
@@ -48,7 +43,6 @@ export function BuildSettingsWindow({
|
||||
<div className="build-settings-window-content">
|
||||
<BuildSettingsPanel
|
||||
projectPath={projectPath}
|
||||
locale={locale}
|
||||
buildService={buildService}
|
||||
sceneManager={sceneManager}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Cpu } from 'lucide-react';
|
||||
import { ICompiler, CompileResult, CompilerContext } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/CompileDialog.css';
|
||||
|
||||
interface CompileDialogProps<TOptions = unknown> {
|
||||
@@ -18,6 +19,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
context,
|
||||
initialOptions
|
||||
}: CompileDialogProps<TOptions>) {
|
||||
const { t } = useLocale();
|
||||
const [options, setOptions] = useState<TOptions>(initialOptions as TOptions);
|
||||
const [isCompiling, setIsCompiling] = useState(false);
|
||||
const [result, setResult] = useState<CompileResult | null>(null);
|
||||
@@ -54,7 +56,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
} catch (error) {
|
||||
setResult({
|
||||
success: false,
|
||||
message: `编译失败: ${error}`,
|
||||
message: `${t('compileDialog.compileFailed')}: ${error}`,
|
||||
errors: [String(error)]
|
||||
});
|
||||
} finally {
|
||||
@@ -97,7 +99,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
</div>
|
||||
{result.outputFiles && result.outputFiles.length > 0 && (
|
||||
<div className="compile-dialog-output-files">
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>输出文件:</div>
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>{t('compileDialog.outputFiles')}:</div>
|
||||
{result.outputFiles.map((file, index) => (
|
||||
<div key={index} className="compile-dialog-output-file">
|
||||
{file}
|
||||
@@ -107,7 +109,7 @@ export function CompileDialog<TOptions = unknown>({
|
||||
)}
|
||||
{result.errors && result.errors.length > 0 && (
|
||||
<div className="compile-dialog-errors">
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>错误:</div>
|
||||
<div style={{ fontWeight: 600, marginBottom: '8px' }}>{t('compileDialog.errors')}:</div>
|
||||
{result.errors.map((error, index) => (
|
||||
<div key={index} className="compile-dialog-error-item">
|
||||
{error}
|
||||
@@ -125,14 +127,14 @@ export function CompileDialog<TOptions = unknown>({
|
||||
className="compile-dialog-btn compile-dialog-btn-cancel"
|
||||
disabled={isCompiling}
|
||||
>
|
||||
关闭
|
||||
{t('compileDialog.close')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCompile}
|
||||
className="compile-dialog-btn compile-dialog-btn-primary"
|
||||
disabled={isCompiling || !!validationError}
|
||||
>
|
||||
{isCompiling ? '编译中...' : '编译'}
|
||||
{isCompiling ? t('compileDialog.compiling') : t('compileDialog.compile')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CompilerRegistry, ICompiler, CompilerContext, CompileResult, IFileSyste
|
||||
import { X, Play, Loader2 } from 'lucide-react';
|
||||
import { open as tauriOpen, save as tauriSave, message as tauriMessage, confirm as tauriConfirm } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke, convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/CompilerConfigDialog.css';
|
||||
|
||||
interface DirectoryEntry {
|
||||
@@ -29,6 +30,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
onClose,
|
||||
onCompileComplete
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [compiler, setCompiler] = useState<ICompiler | null>(null);
|
||||
const [options, setOptions] = useState<unknown>(null);
|
||||
const [isCompiling, setIsCompiling] = useState(false);
|
||||
@@ -164,7 +166,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
} catch (error) {
|
||||
setCompileResult({
|
||||
success: false,
|
||||
message: `编译失败: ${error}`,
|
||||
message: t('compilerConfig.compileFailed', { error: String(error) }),
|
||||
errors: [String(error)]
|
||||
});
|
||||
} finally {
|
||||
@@ -180,7 +182,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
<div className="compiler-dialog-overlay">
|
||||
<div className="compiler-dialog">
|
||||
<div className="compiler-dialog-header">
|
||||
<h3>{compiler?.name || '编译器配置'}</h3>
|
||||
<h3>{compiler?.name || t('compilerConfig.title')}</h3>
|
||||
<button className="close-button" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -191,7 +193,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
compiler.createConfigUI(handleOptionsChange, context)
|
||||
) : (
|
||||
<div className="no-config">
|
||||
{compiler ? '该编译器没有配置界面' : '编译器未找到'}
|
||||
{compiler ? t('compilerConfig.noConfigUI') : t('compilerConfig.compilerNotFound')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -201,7 +203,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
<div className="result-message">{compileResult.message}</div>
|
||||
{compileResult.outputFiles && compileResult.outputFiles.length > 0 && (
|
||||
<div className="output-files">
|
||||
已生成 {compileResult.outputFiles.length} 个文件
|
||||
{t('compilerConfig.generatedFiles', { count: compileResult.outputFiles.length })}
|
||||
</div>
|
||||
)}
|
||||
{compileResult.errors && compileResult.errors.length > 0 && (
|
||||
@@ -220,7 +222,7 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
onClick={onClose}
|
||||
disabled={isCompiling}
|
||||
>
|
||||
取消
|
||||
{t('compilerConfig.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="compile-button"
|
||||
@@ -230,12 +232,12 @@ export const CompilerConfigDialog: React.FC<CompilerConfigDialogProps> = ({
|
||||
{isCompiling ? (
|
||||
<>
|
||||
<Loader2 size={16} className="spinning" />
|
||||
编译中...
|
||||
{t('compilerConfig.compiling')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={16} />
|
||||
编译
|
||||
{t('compilerConfig.compile')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import {
|
||||
Plus,
|
||||
Download,
|
||||
@@ -162,6 +163,7 @@ export function ContentBrowser({
|
||||
onDockInLayout,
|
||||
revealPath
|
||||
}: ContentBrowserProps) {
|
||||
const { t } = useLocale();
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const fileActionRegistry = Core.services.resolve(FileActionRegistry);
|
||||
|
||||
@@ -240,93 +242,6 @@ export function ContentBrowser({
|
||||
}
|
||||
}, [fileActionRegistry, messageHub]);
|
||||
|
||||
const t = {
|
||||
en: {
|
||||
favorites: 'Favorites',
|
||||
collections: 'Collections',
|
||||
add: 'Add',
|
||||
import: 'Import',
|
||||
saveAll: 'Save All',
|
||||
search: 'Search',
|
||||
items: 'items',
|
||||
dockInLayout: 'Dock in Layout',
|
||||
noProject: 'No project loaded',
|
||||
empty: 'This folder is empty',
|
||||
newFolder: 'New Folder',
|
||||
newPrefix: 'New',
|
||||
managedDirectoryTooltip: 'GUID-managed directory - Assets here get unique IDs for references',
|
||||
unmanagedWarning: 'This folder is not managed by GUID system. Assets created here cannot be referenced by GUID.',
|
||||
unmanagedWarningTitle: 'Unmanaged Directory',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
openInExplorer: 'Show in Explorer',
|
||||
copyPath: 'Copy Path',
|
||||
newSubfolder: 'New Subfolder',
|
||||
deleteConfirmTitle: 'Confirm Delete',
|
||||
deleteConfirmMessage: 'Are you sure you want to delete',
|
||||
cannotDeleteRoot: 'Cannot delete root directory'
|
||||
},
|
||||
zh: {
|
||||
favorites: '收藏夹',
|
||||
collections: '收藏集',
|
||||
add: '添加',
|
||||
import: '导入',
|
||||
saveAll: '全部保存',
|
||||
search: '搜索',
|
||||
items: '项',
|
||||
dockInLayout: '停靠到布局',
|
||||
noProject: '未加载项目',
|
||||
empty: '文件夹为空',
|
||||
newFolder: '新建文件夹',
|
||||
newPrefix: '新建',
|
||||
managedDirectoryTooltip: 'GUID 管理的目录 - 此处的资产会获得唯一 ID 以便引用',
|
||||
unmanagedWarning: '此文件夹不受 GUID 系统管理。在此创建的资产无法通过 GUID 引用。',
|
||||
unmanagedWarningTitle: '非托管目录',
|
||||
rename: '重命名',
|
||||
delete: '删除',
|
||||
openInExplorer: '在资源管理器中显示',
|
||||
copyPath: '复制路径',
|
||||
newSubfolder: '新建子文件夹',
|
||||
deleteConfirmTitle: '确认删除',
|
||||
deleteConfirmMessage: '确定要删除',
|
||||
cannotDeleteRoot: '无法删除根目录'
|
||||
}
|
||||
}[locale] || {
|
||||
favorites: 'Favorites',
|
||||
collections: 'Collections',
|
||||
add: 'Add',
|
||||
import: 'Import',
|
||||
saveAll: 'Save All',
|
||||
search: 'Search',
|
||||
items: 'items',
|
||||
dockInLayout: 'Dock in Layout',
|
||||
noProject: 'No project loaded',
|
||||
empty: 'This folder is empty',
|
||||
newFolder: 'New Folder',
|
||||
newPrefix: 'New',
|
||||
managedDirectoryTooltip: 'GUID-managed directory - Assets here get unique IDs for references',
|
||||
unmanagedWarning: 'This folder is not managed by GUID system. Assets created here cannot be referenced by GUID.',
|
||||
unmanagedWarningTitle: 'Unmanaged Directory',
|
||||
rename: 'Rename',
|
||||
delete: 'Delete',
|
||||
openInExplorer: 'Show in Explorer',
|
||||
copyPath: 'Copy Path',
|
||||
newSubfolder: 'New Subfolder',
|
||||
deleteConfirmTitle: 'Confirm Delete',
|
||||
deleteConfirmMessage: 'Are you sure you want to delete',
|
||||
cannotDeleteRoot: 'Cannot delete root directory'
|
||||
};
|
||||
|
||||
// 文件创建模板的 label 本地化映射
|
||||
const templateLabels: Record<string, { en: string; zh: string }> = {
|
||||
'Material': { en: 'Material', zh: '材质' },
|
||||
'Shader': { en: 'Shader', zh: '着色器' },
|
||||
'Tilemap': { en: 'Tilemap', zh: '瓦片地图' },
|
||||
'Tileset': { en: 'Tileset', zh: '瓦片集' },
|
||||
'Component': { en: 'Component', zh: '组件' },
|
||||
'System': { en: 'System', zh: '系统' },
|
||||
'TypeScript': { en: 'TypeScript', zh: 'TypeScript' },
|
||||
};
|
||||
|
||||
// 注册内置的 TypeScript 文件创建模板
|
||||
// Register built-in TypeScript file creation templates
|
||||
@@ -566,11 +481,20 @@ export class ${className} {
|
||||
}, [selectedPaths, assets, renameDialog, deleteConfirmDialog, createFileDialog]);
|
||||
|
||||
const getTemplateLabel = (label: string): string => {
|
||||
const mapping = templateLabels[label];
|
||||
if (mapping) {
|
||||
return locale === 'zh' ? mapping.zh : mapping.en;
|
||||
}
|
||||
return label;
|
||||
// Map template labels to translation keys
|
||||
const keyMap: Record<string, string> = {
|
||||
'Material': 'contentBrowser.templateLabels.material',
|
||||
'Shader': 'contentBrowser.templateLabels.shader',
|
||||
'Tilemap': 'contentBrowser.templateLabels.tilemap',
|
||||
'Tileset': 'contentBrowser.templateLabels.tileset',
|
||||
'Component': 'contentBrowser.templateLabels.component',
|
||||
'System': 'contentBrowser.templateLabels.system',
|
||||
'TypeScript': 'contentBrowser.templateLabels.typescript',
|
||||
'Inspector': 'contentBrowser.templateLabels.inspector',
|
||||
'Gizmo': 'contentBrowser.templateLabels.gizmo'
|
||||
};
|
||||
const key = keyMap[label];
|
||||
return key ? t(key) : label;
|
||||
};
|
||||
|
||||
// Build folder tree - use ref to avoid dependency cycle
|
||||
@@ -1106,7 +1030,7 @@ export class ${className} {
|
||||
// Show warning header if current path is not managed
|
||||
if (!isCurrentPathManaged && currentPath) {
|
||||
items.push({
|
||||
label: t.unmanagedWarningTitle,
|
||||
label: t('contentBrowser.unmanagedWarningTitle'),
|
||||
icon: <AlertTriangle size={16} className="warning-icon" />,
|
||||
disabled: true,
|
||||
onClick: () => {}
|
||||
@@ -1115,7 +1039,7 @@ export class ${className} {
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: t.newFolder,
|
||||
label: t('contentBrowser.newFolder'),
|
||||
icon: <FolderClosed size={16} />,
|
||||
onClick: async () => {
|
||||
if (!currentPath) return;
|
||||
@@ -1163,7 +1087,7 @@ export class ${className} {
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '在资源管理器中显示' : 'Show in Explorer',
|
||||
label: t('contentBrowser.openInExplorer'),
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
if (currentPath) {
|
||||
@@ -1178,7 +1102,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '刷新' : 'Refresh',
|
||||
label: t('contentBrowser.refresh'),
|
||||
icon: <RefreshCw size={16} />,
|
||||
onClick: async () => {
|
||||
if (currentPath) {
|
||||
@@ -1194,16 +1118,15 @@ export class ${className} {
|
||||
// Asset context menu
|
||||
if (asset.type === 'file') {
|
||||
items.push({
|
||||
label: locale === 'zh' ? '打开' : 'Open',
|
||||
label: t('contentBrowser.open'),
|
||||
icon: <File size={16} />,
|
||||
onClick: () => handleAssetDoubleClick(asset)
|
||||
});
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 保存
|
||||
items.push({
|
||||
label: locale === 'zh' ? '保存' : 'Save',
|
||||
label: t('contentBrowser.save'),
|
||||
icon: <Save size={16} />,
|
||||
shortcut: 'Ctrl+S',
|
||||
onClick: () => {
|
||||
@@ -1212,9 +1135,8 @@ export class ${className} {
|
||||
});
|
||||
}
|
||||
|
||||
// 重命名
|
||||
items.push({
|
||||
label: locale === 'zh' ? '重命名' : 'Rename',
|
||||
label: t('contentBrowser.rename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'F2',
|
||||
onClick: () => {
|
||||
@@ -1223,9 +1145,8 @@ export class ${className} {
|
||||
}
|
||||
});
|
||||
|
||||
// 批量重命名
|
||||
items.push({
|
||||
label: locale === 'zh' ? '批量重命名' : 'Batch Rename',
|
||||
label: t('contentBrowser.batchRename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'Shift+F2',
|
||||
disabled: true,
|
||||
@@ -1234,9 +1155,8 @@ export class ${className} {
|
||||
}
|
||||
});
|
||||
|
||||
// 复制
|
||||
items.push({
|
||||
label: locale === 'zh' ? '复制' : 'Duplicate',
|
||||
label: t('contentBrowser.duplicate'),
|
||||
icon: <Clipboard size={16} />,
|
||||
shortcut: 'Ctrl+D',
|
||||
onClick: () => {
|
||||
@@ -1244,9 +1164,8 @@ export class ${className} {
|
||||
}
|
||||
});
|
||||
|
||||
// 删除
|
||||
items.push({
|
||||
label: locale === 'zh' ? '删除' : 'Delete',
|
||||
label: t('contentBrowser.delete'),
|
||||
icon: <Trash2 size={16} />,
|
||||
shortcut: 'Delete',
|
||||
onClick: () => {
|
||||
@@ -1257,21 +1176,20 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 资产操作子菜单
|
||||
items.push({
|
||||
label: locale === 'zh' ? '资产操作' : 'Asset Actions',
|
||||
label: t('contentBrowser.assetActions'),
|
||||
icon: <Settings size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: locale === 'zh' ? '重新导入' : 'Reimport',
|
||||
label: t('contentBrowser.reimport'),
|
||||
icon: <RefreshCw size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Reimport asset:', asset.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: locale === 'zh' ? '导出...' : 'Export...',
|
||||
label: t('contentBrowser.export'),
|
||||
icon: <Package size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Export asset:', asset.path);
|
||||
@@ -1279,7 +1197,7 @@ export class ${className} {
|
||||
},
|
||||
{ label: '', separator: true, onClick: () => {} },
|
||||
{
|
||||
label: locale === 'zh' ? '迁移资产' : 'Migrate Asset',
|
||||
label: t('contentBrowser.migrateAsset'),
|
||||
icon: <Folder size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Migrate asset:', asset.path);
|
||||
@@ -1288,26 +1206,25 @@ export class ${className} {
|
||||
]
|
||||
});
|
||||
|
||||
// 资产本地化子菜单
|
||||
items.push({
|
||||
label: locale === 'zh' ? '资产本地化' : 'Asset Localization',
|
||||
label: t('contentBrowser.assetLocalization'),
|
||||
icon: <Globe size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: locale === 'zh' ? '创建本地化资产' : 'Create Localized Asset',
|
||||
label: t('contentBrowser.createLocalizedAsset'),
|
||||
onClick: () => {
|
||||
console.log('Create localized asset:', asset.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: locale === 'zh' ? '导入翻译' : 'Import Translation',
|
||||
label: t('contentBrowser.importTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Import translation:', asset.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: locale === 'zh' ? '导出翻译' : 'Export Translation',
|
||||
label: t('contentBrowser.exportTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Export translation:', asset.path);
|
||||
}
|
||||
@@ -1317,9 +1234,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 标签管理
|
||||
items.push({
|
||||
label: locale === 'zh' ? '管理标签' : 'Manage Tags',
|
||||
label: t('contentBrowser.manageTags'),
|
||||
icon: <Tag size={16} />,
|
||||
shortcut: 'Ctrl+T',
|
||||
onClick: () => {
|
||||
@@ -1329,9 +1245,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 路径复制选项
|
||||
items.push({
|
||||
label: locale === 'zh' ? '复制引用' : 'Copy Reference',
|
||||
label: t('contentBrowser.copyReference'),
|
||||
icon: <Link size={16} />,
|
||||
shortcut: 'Ctrl+C',
|
||||
onClick: () => {
|
||||
@@ -1340,7 +1255,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '拷贝Object路径' : 'Copy Object Path',
|
||||
label: t('contentBrowser.copyObjectPath'),
|
||||
icon: <Copy size={16} />,
|
||||
shortcut: 'Ctrl+Shift+C',
|
||||
onClick: () => {
|
||||
@@ -1350,7 +1265,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '拷贝包路径' : 'Copy Package Path',
|
||||
label: t('contentBrowser.copyPackagePath'),
|
||||
icon: <Package size={16} />,
|
||||
shortcut: 'Ctrl+Alt+C',
|
||||
onClick: () => {
|
||||
@@ -1361,9 +1276,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 引用查看器
|
||||
items.push({
|
||||
label: locale === 'zh' ? '引用查看器' : 'Reference Viewer',
|
||||
label: t('contentBrowser.referenceViewer'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+R',
|
||||
onClick: () => {
|
||||
@@ -1372,7 +1286,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: locale === 'zh' ? '尺寸信息图' : 'Size Map',
|
||||
label: t('contentBrowser.sizeMap'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+D',
|
||||
onClick: () => {
|
||||
@@ -1382,9 +1296,8 @@ export class ${className} {
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 在文件管理器中显示
|
||||
items.push({
|
||||
label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer',
|
||||
label: t('contentBrowser.openInExplorer'),
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1397,7 +1310,7 @@ export class ${className} {
|
||||
});
|
||||
|
||||
return items;
|
||||
}, [currentPath, fileCreationTemplates, handleAssetDoubleClick, loadAssets, locale, t.newFolder, t.newPrefix, t.unmanagedWarningTitle, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog, projectPath]);
|
||||
}, [currentPath, fileCreationTemplates, handleAssetDoubleClick, loadAssets, t, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog, projectPath, getTemplateLabel]);
|
||||
|
||||
/**
|
||||
* Handle folder tree context menu
|
||||
@@ -1414,7 +1327,7 @@ export class ${className} {
|
||||
|
||||
// New subfolder
|
||||
items.push({
|
||||
label: t.newSubfolder,
|
||||
label: t('contentBrowser.newSubfolder'),
|
||||
icon: <FolderClosed size={16} />,
|
||||
onClick: async () => {
|
||||
const folderPath = `${node.path}/New Folder`;
|
||||
@@ -1434,7 +1347,7 @@ export class ${className} {
|
||||
// Rename (not for root)
|
||||
if (!isRoot) {
|
||||
items.push({
|
||||
label: t.rename,
|
||||
label: t('contentBrowser.rename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
onClick: () => {
|
||||
setRenameDialog({
|
||||
@@ -1452,7 +1365,7 @@ export class ${className} {
|
||||
// Delete (not for root)
|
||||
if (!isRoot) {
|
||||
items.push({
|
||||
label: t.delete,
|
||||
label: t('contentBrowser.delete'),
|
||||
icon: <Trash2 size={16} />,
|
||||
onClick: () => {
|
||||
setDeleteConfirmDialog({
|
||||
@@ -1464,7 +1377,7 @@ export class ${className} {
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
label: t.cannotDeleteRoot,
|
||||
label: t('contentBrowser.cannotDeleteRoot'),
|
||||
icon: <Trash2 size={16} />,
|
||||
disabled: true,
|
||||
onClick: () => {}
|
||||
@@ -1475,7 +1388,7 @@ export class ${className} {
|
||||
|
||||
// Copy path
|
||||
items.push({
|
||||
label: t.copyPath,
|
||||
label: t('contentBrowser.copyPath'),
|
||||
icon: <Clipboard size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1488,7 +1401,7 @@ export class ${className} {
|
||||
|
||||
// Show in explorer
|
||||
items.push({
|
||||
label: t.openInExplorer,
|
||||
label: t('contentBrowser.openInExplorer'),
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1503,7 +1416,7 @@ export class ${className} {
|
||||
position: { x: e.clientX, y: e.clientY },
|
||||
items
|
||||
});
|
||||
}, [projectPath, t, refreshAll, setRenameDialog, setDeleteConfirmDialog, setFolderTreeContextMenu, setExpandedFolders]);
|
||||
}, [projectPath, t, refreshAll, setRenameDialog, setDeleteConfirmDialog, setFolderTreeContextMenu, setExpandedFolders, getTemplateLabel]);
|
||||
|
||||
// Render folder tree node
|
||||
const renderFolderNode = useCallback((node: FolderNode, depth: number = 0) => {
|
||||
@@ -1521,7 +1434,7 @@ export class ${className} {
|
||||
style={{ paddingLeft: `${depth * 16 + 8}px` }}
|
||||
onClick={() => handleFolderSelect(node.path)}
|
||||
onContextMenu={(e) => handleFolderTreeContextMenu(e, node)}
|
||||
title={isRootManaged ? t.managedDirectoryTooltip : undefined}
|
||||
title={isRootManaged ? t('contentBrowser.managedDirectoryTooltip') : undefined}
|
||||
onDragOver={(e) => handleFolderDragOver(e, node.path)}
|
||||
onDragLeave={handleFolderDragLeave}
|
||||
onDrop={(e) => handleFolderDrop(e, node.path)}
|
||||
@@ -1545,13 +1458,13 @@ export class ${className} {
|
||||
</span>
|
||||
<span className="folder-tree-name">{node.name}</span>
|
||||
{isRootManaged && (
|
||||
<span className="managed-badge" title={t.managedDirectoryTooltip}>GUID</span>
|
||||
<span className="managed-badge" title={t('contentBrowser.managedDirectoryTooltip')}>GUID</span>
|
||||
)}
|
||||
</div>
|
||||
{isExpanded && node.children.map(child => renderFolderNode(child, depth + 1))}
|
||||
</div>
|
||||
);
|
||||
}, [currentPath, expandedFolders, handleFolderSelect, handleFolderTreeContextMenu, toggleFolderExpand, projectPath, t.managedDirectoryTooltip, dragOverFolder, handleFolderDragOver, handleFolderDragLeave, handleFolderDrop]);
|
||||
}, [currentPath, expandedFolders, handleFolderSelect, handleFolderTreeContextMenu, toggleFolderExpand, projectPath, t, dragOverFolder, handleFolderDragOver, handleFolderDragLeave, handleFolderDrop]);
|
||||
|
||||
// Filter assets by search
|
||||
const filteredAssets = searchQuery.trim()
|
||||
@@ -1564,7 +1477,7 @@ export class ${className} {
|
||||
return (
|
||||
<div className="content-browser">
|
||||
<div className="content-browser-empty">
|
||||
<p>{t.noProject}</p>
|
||||
<p>{t('contentBrowser.noProject')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1585,7 +1498,7 @@ export class ${className} {
|
||||
onClick={() => setFavoritesExpanded(!favoritesExpanded)}
|
||||
>
|
||||
{favoritesExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span>{t.favorites}</span>
|
||||
<span>{t('contentBrowser.favorites')}</span>
|
||||
<button className="cb-section-btn" onClick={(e) => e.stopPropagation()}>
|
||||
<Search size={12} />
|
||||
</button>
|
||||
@@ -1620,7 +1533,7 @@ export class ${className} {
|
||||
onClick={() => setCollectionsExpanded(!collectionsExpanded)}
|
||||
>
|
||||
{collectionsExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
<span>{t.collections}</span>
|
||||
<span>{t('contentBrowser.collections')}</span>
|
||||
<div className="cb-section-actions">
|
||||
<button className="cb-section-btn" onClick={(e) => e.stopPropagation()}>
|
||||
<Plus size={12} />
|
||||
@@ -1645,15 +1558,15 @@ export class ${className} {
|
||||
<div className="cb-toolbar-left">
|
||||
<button className="cb-toolbar-btn primary">
|
||||
<Plus size={14} />
|
||||
<span>{t.add}</span>
|
||||
<span>{t('contentBrowser.add')}</span>
|
||||
</button>
|
||||
<button className="cb-toolbar-btn">
|
||||
<Download size={14} />
|
||||
<span>{t.import}</span>
|
||||
<span>{t('contentBrowser.import')}</span>
|
||||
</button>
|
||||
<button className="cb-toolbar-btn">
|
||||
<Save size={14} />
|
||||
<span>{t.saveAll}</span>
|
||||
<span>{t('contentBrowser.saveAll')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1677,10 +1590,10 @@ export class ${className} {
|
||||
<button
|
||||
className="cb-toolbar-btn dock-btn"
|
||||
onClick={onDockInLayout}
|
||||
title={t.dockInLayout}
|
||||
title={t('contentBrowser.dockInLayout')}
|
||||
>
|
||||
<PanelRightClose size={14} />
|
||||
<span>{t.dockInLayout}</span>
|
||||
<span>{t('contentBrowser.dockInLayout')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1697,7 +1610,7 @@ export class ${className} {
|
||||
<input
|
||||
type="text"
|
||||
className="cb-search-input"
|
||||
placeholder={`${t.search} ${breadcrumbs[breadcrumbs.length - 1]?.name || ''}`}
|
||||
placeholder={`${t('contentBrowser.search')} ${breadcrumbs[breadcrumbs.length - 1]?.name || ''}`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -1726,7 +1639,7 @@ export class ${className} {
|
||||
{loading ? (
|
||||
<div className="cb-loading">Loading...</div>
|
||||
) : filteredAssets.length === 0 ? (
|
||||
<div className="cb-empty">{t.empty}</div>
|
||||
<div className="cb-empty">{t('contentBrowser.empty')}</div>
|
||||
) : (
|
||||
filteredAssets.map(asset => {
|
||||
const isDragOverAsset = asset.type === 'folder' && dragOverFolder === asset.path;
|
||||
@@ -1793,7 +1706,7 @@ export class ${className} {
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="cb-status-bar">
|
||||
<span>{filteredAssets.length} {t.items}</span>
|
||||
<span>{filteredAssets.length} {t('contentBrowser.items')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1820,7 +1733,7 @@ export class ${className} {
|
||||
<div className="cb-dialog-overlay" onClick={() => setRenameDialog(null)}>
|
||||
<div className="cb-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="cb-dialog-header">
|
||||
<h3>{locale === 'zh' ? '重命名' : 'Rename'}</h3>
|
||||
<h3>{t('contentBrowser.dialogs.renameTitle')}</h3>
|
||||
</div>
|
||||
<div className="cb-dialog-body">
|
||||
<input
|
||||
@@ -1836,13 +1749,13 @@ export class ${className} {
|
||||
</div>
|
||||
<div className="cb-dialog-footer">
|
||||
<button className="cb-btn" onClick={() => setRenameDialog(null)}>
|
||||
{locale === 'zh' ? '取消' : 'Cancel'}
|
||||
{t('contentBrowser.dialogs.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="cb-btn primary"
|
||||
onClick={() => handleRename(renameDialog.asset, renameDialog.newName)}
|
||||
>
|
||||
{locale === 'zh' ? '确定' : 'OK'}
|
||||
{t('contentBrowser.dialogs.ok')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1854,24 +1767,22 @@ export class ${className} {
|
||||
<div className="cb-dialog-overlay" onClick={() => setDeleteConfirmDialog(null)}>
|
||||
<div className="cb-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="cb-dialog-header">
|
||||
<h3>{locale === 'zh' ? '确认删除' : 'Confirm Delete'}</h3>
|
||||
<h3>{t('contentBrowser.deleteConfirmTitle')}</h3>
|
||||
</div>
|
||||
<div className="cb-dialog-body">
|
||||
<p>
|
||||
{locale === 'zh'
|
||||
? `确定要删除 "${deleteConfirmDialog.name}" 吗?`
|
||||
: `Delete "${deleteConfirmDialog.name}"?`}
|
||||
{t('contentBrowser.deleteConfirmMessage', { name: deleteConfirmDialog.name })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="cb-dialog-footer">
|
||||
<button className="cb-btn" onClick={() => setDeleteConfirmDialog(null)}>
|
||||
{locale === 'zh' ? '取消' : 'Cancel'}
|
||||
{t('contentBrowser.dialogs.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="cb-btn danger"
|
||||
onClick={() => handleDelete(deleteConfirmDialog)}
|
||||
>
|
||||
{locale === 'zh' ? '删除' : 'Delete'}
|
||||
{t('contentBrowser.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1887,11 +1798,11 @@ export class ${className} {
|
||||
: `.${createFileDialog.template.extension}`;
|
||||
return (
|
||||
<PromptDialog
|
||||
title={locale === 'zh' ? `新建 ${getTemplateLabel(createFileDialog.template.label)}` : `New ${createFileDialog.template.label}`}
|
||||
message={locale === 'zh' ? `输入文件名(将添加 ${ext}):` : `Enter file name (${ext} will be added):`}
|
||||
title={t('contentBrowser.dialogs.newFile', { type: getTemplateLabel(createFileDialog.template.label) })}
|
||||
message={t('contentBrowser.dialogs.enterFileName', { ext })}
|
||||
placeholder="filename"
|
||||
confirmText={locale === 'zh' ? '创建' : 'Create'}
|
||||
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
|
||||
confirmText={t('contentBrowser.dialogs.create')}
|
||||
cancelText={t('contentBrowser.dialogs.cancel')}
|
||||
onConfirm={async (value) => {
|
||||
const { parentPath, template } = createFileDialog;
|
||||
setCreateFileDialog(null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, File, FolderTree, FolderOpen } from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/ExportRuntimeDialog.css';
|
||||
|
||||
interface ExportRuntimeDialogProps {
|
||||
@@ -33,6 +34,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
currentFileName,
|
||||
projectPath
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [selectedMode, setSelectedMode] = useState<'single' | 'workspace'>('workspace');
|
||||
const [assetOutputPath, setAssetOutputPath] = useState('');
|
||||
const [typeOutputPath, setTypeOutputPath] = useState('');
|
||||
@@ -116,7 +118,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: '选择资产输出目录',
|
||||
title: t('exportRuntime.selectAssetDir'),
|
||||
defaultPath: assetOutputPath || projectPath
|
||||
});
|
||||
if (selected) {
|
||||
@@ -134,7 +136,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: '选择类型定义输出目录',
|
||||
title: t('exportRuntime.selectTypeDir'),
|
||||
defaultPath: typeOutputPath || projectPath
|
||||
});
|
||||
if (selected) {
|
||||
@@ -149,22 +151,22 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!assetOutputPath) {
|
||||
setExportMessage('错误:请选择资产输出路径');
|
||||
setExportMessage(t('exportRuntime.errorSelectAssetPath'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!typeOutputPath) {
|
||||
setExportMessage('错误:请选择类型定义输出路径');
|
||||
setExportMessage(t('exportRuntime.errorSelectTypePath'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMode === 'workspace' && selectedFiles.size === 0) {
|
||||
setExportMessage('错误:请至少选择一个文件');
|
||||
setExportMessage(t('exportRuntime.errorSelectFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedMode === 'single' && !currentFileName) {
|
||||
setExportMessage('错误:没有可导出的当前文件');
|
||||
setExportMessage(t('exportRuntime.errorNoCurrentFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -174,7 +176,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
|
||||
setIsExporting(true);
|
||||
setExportProgress(0);
|
||||
setExportMessage('正在导出...');
|
||||
setExportMessage(t('exportRuntime.exporting'));
|
||||
|
||||
try {
|
||||
await onExport({
|
||||
@@ -186,9 +188,9 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
});
|
||||
|
||||
setExportProgress(100);
|
||||
setExportMessage('导出成功!');
|
||||
setExportMessage(t('exportRuntime.exportSuccess'));
|
||||
} catch (error) {
|
||||
setExportMessage(`导出失败:${error}`);
|
||||
setExportMessage(t('exportRuntime.exportFailed', { error: String(error) }));
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -198,7 +200,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
<div className="export-dialog-overlay">
|
||||
<div className="export-dialog" style={{ maxWidth: '700px', width: '90%' }}>
|
||||
<div className="export-dialog-header">
|
||||
<h3>导出运行时资产</h3>
|
||||
<h3>{t('exportRuntime.title')}</h3>
|
||||
<button onClick={onClose} className="export-dialog-close">
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -213,26 +215,26 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
disabled={!hasProject}
|
||||
>
|
||||
<FolderTree size={16} />
|
||||
工作区导出
|
||||
{t('exportRuntime.workspaceExport')}
|
||||
</button>
|
||||
<button
|
||||
className={`export-mode-tab ${selectedMode === 'single' ? 'active' : ''}`}
|
||||
onClick={() => setSelectedMode('single')}
|
||||
>
|
||||
<File size={16} />
|
||||
当前文件
|
||||
{t('exportRuntime.currentFile')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 资产输出路径 */}
|
||||
<div className="export-section">
|
||||
<h4>资产输出路径</h4>
|
||||
<h4>{t('exportRuntime.assetOutputPath')}</h4>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={assetOutputPath}
|
||||
onChange={(e) => setAssetOutputPath(e.target.value)}
|
||||
placeholder="选择资产输出目录(.btree.bin / .btree.json)..."
|
||||
placeholder={t('exportRuntime.selectAssetDirPlaceholder')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
@@ -259,31 +261,26 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
浏览
|
||||
{t('exportRuntime.browse')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TypeScript 类型定义输出路径 */}
|
||||
<div className="export-section">
|
||||
<h4>TypeScript 类型定义输出路径</h4>
|
||||
<div style={{ marginBottom: '12px', fontSize: '11px', color: '#999', lineHeight: '1.5' }}>
|
||||
{selectedMode === 'workspace' ? (
|
||||
<>
|
||||
将导出以下类型定义:<br />
|
||||
• 每个行为树的黑板变量类型(.d.ts)<br />
|
||||
• 全局黑板变量类型(GlobalBlackboard.ts)
|
||||
</>
|
||||
) : (
|
||||
'将导出当前行为树的黑板变量类型(.d.ts)'
|
||||
)}
|
||||
<h4>{t('exportRuntime.typeOutputPath')}</h4>
|
||||
<div style={{ marginBottom: '12px', fontSize: '11px', color: '#999', lineHeight: '1.5', whiteSpace: 'pre-line' }}>
|
||||
{selectedMode === 'workspace'
|
||||
? t('exportRuntime.typeOutputHintWorkspace')
|
||||
: t('exportRuntime.typeOutputHintSingle')
|
||||
}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={typeOutputPath}
|
||||
onChange={(e) => setTypeOutputPath(e.target.value)}
|
||||
placeholder="选择类型定义输出目录..."
|
||||
placeholder={t('exportRuntime.selectTypeDirPlaceholder')}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
@@ -310,7 +307,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
浏览
|
||||
{t('exportRuntime.browse')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -320,7 +317,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
<div className="export-section">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<h4 style={{ margin: 0, fontSize: '13px', color: '#ccc' }}>
|
||||
选择要导出的文件 ({selectedFiles.size}/{availableFiles.length})
|
||||
{t('exportRuntime.selectFilesToExport')} ({selectedFiles.size}/{availableFiles.length})
|
||||
</h4>
|
||||
<button
|
||||
onClick={handleSelectAll}
|
||||
@@ -334,7 +331,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
fontSize: '12px'
|
||||
}}
|
||||
>
|
||||
{selectAll ? '取消全选' : '全选'}
|
||||
{selectAll ? t('exportRuntime.deselectAll') : t('exportRuntime.selectAll')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="export-file-list">
|
||||
@@ -359,8 +356,8 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
onChange={(e) => handleFileFormatChange(file, e.target.value as 'json' | 'binary')}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<option value="binary">二进制</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="binary">{t('exportRuntime.binary')}</option>
|
||||
<option value="json">{t('exportRuntime.json')}</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
@@ -371,7 +368,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
{/* 单文件模式 */}
|
||||
{selectedMode === 'single' && (
|
||||
<div className="export-section">
|
||||
<h4>当前文件</h4>
|
||||
<h4>{t('exportRuntime.currentFile')}</h4>
|
||||
{currentFileName ? (
|
||||
<div className="export-file-list">
|
||||
<div className="export-file-item selected">
|
||||
@@ -384,8 +381,8 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
value={fileFormats.get(currentFileName) || 'binary'}
|
||||
onChange={(e) => handleFileFormatChange(currentFileName, e.target.value as 'json' | 'binary')}
|
||||
>
|
||||
<option value="binary">二进制</option>
|
||||
<option value="json">JSON</option>
|
||||
<option value="binary">{t('exportRuntime.binary')}</option>
|
||||
<option value="json">{t('exportRuntime.json')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -400,9 +397,9 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
border: '1px solid #3a3a3a'
|
||||
}}>
|
||||
<File size={32} style={{ margin: '0 auto 12px', opacity: 0.5 }} />
|
||||
<div>没有打开的行为树文件</div>
|
||||
<div>{t('exportRuntime.noOpenFile')}</div>
|
||||
<div style={{ fontSize: '11px', marginTop: '8px' }}>
|
||||
请先在编辑器中打开一个行为树文件
|
||||
{t('exportRuntime.openFileHint')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -415,7 +412,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
<div style={{
|
||||
flex: 1,
|
||||
fontSize: '12px',
|
||||
color: exportMessage.startsWith('错误') ? '#f48771' : exportMessage.includes('成功') ? '#89d185' : '#ccc',
|
||||
color: exportMessage.includes('Error') || exportMessage.includes('错误') ? '#f48771' : exportMessage.includes('success') || exportMessage.includes('成功') ? '#89d185' : '#ccc',
|
||||
paddingLeft: '8px'
|
||||
}}>
|
||||
{exportMessage}
|
||||
@@ -439,7 +436,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<button onClick={onClose} className="export-dialog-btn export-dialog-btn-cancel">
|
||||
关闭
|
||||
{t('exportRuntime.close')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExport}
|
||||
@@ -447,7 +444,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
disabled={isExporting}
|
||||
style={{ opacity: isExporting ? 0.5 : 1 }}
|
||||
>
|
||||
{isExporting ? '导出中...' : '导出'}
|
||||
{isExporting ? t('exportRuntime.exporting') : t('exportRuntime.export')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Core } from '@esengine/ecs-framework';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import { PromptDialog } from './PromptDialog';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/FileTree.css';
|
||||
|
||||
/**
|
||||
@@ -56,6 +57,7 @@ export interface FileTreeHandle {
|
||||
}
|
||||
|
||||
export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, onSelectFile, onSelectFiles, selectedPath, selectedPaths, messageHub, searchQuery, showFiles = true, onOpenScene }, ref) => {
|
||||
const { t } = useLocale();
|
||||
const [tree, setTree] = useState<TreeNode[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [internalSelectedPath, setInternalSelectedPath] = useState<string | null>(null);
|
||||
@@ -475,7 +477,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
setNewName('');
|
||||
} catch (error) {
|
||||
console.error('Failed to rename:', error);
|
||||
alert(`重命名失败: ${error}`);
|
||||
alert(`${t('fileTree.renameFailed')}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -499,7 +501,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
await refreshTree();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete:', error);
|
||||
alert(`删除失败: ${error}`);
|
||||
alert(`${t('fileTree.deleteFailed')}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -549,7 +551,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
await refreshTree();
|
||||
} catch (error) {
|
||||
console.error(`Failed to ${type}:`, error);
|
||||
alert(`${type === 'create-file' ? '创建文件' : type === 'create-folder' ? '创建文件夹' : '创建模板文件'}失败: ${error}`);
|
||||
const errorKey = type === 'create-file' ? 'fileTree.createFileFailed' :
|
||||
type === 'create-folder' ? 'fileTree.createFolderFailed' : 'fileTree.createTemplateFailed';
|
||||
alert(`${t(errorKey)}: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -557,12 +561,12 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
if (!node) {
|
||||
const baseItems: ContextMenuItem[] = [
|
||||
{
|
||||
label: '新建文件',
|
||||
label: t('fileTree.newFile'),
|
||||
icon: <FileText size={16} />,
|
||||
onClick: () => rootPath && handleCreateFileClick(rootPath)
|
||||
},
|
||||
{
|
||||
label: '新建文件夹',
|
||||
label: t('fileTree.newFolder'),
|
||||
icon: <FolderPlus size={16} />,
|
||||
onClick: () => rootPath && handleCreateFolderClick(rootPath)
|
||||
}
|
||||
@@ -589,7 +593,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
if (node.type === 'file') {
|
||||
items.push({
|
||||
label: '打开文件',
|
||||
label: t('fileTree.openFile'),
|
||||
icon: <File size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -621,9 +625,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 文件操作菜单项
|
||||
// 文件操作菜单项 | File operation menu items
|
||||
items.push({
|
||||
label: '保存',
|
||||
label: t('fileTree.save'),
|
||||
icon: <Save size={16} />,
|
||||
shortcut: 'Ctrl+S',
|
||||
onClick: () => {
|
||||
@@ -634,7 +638,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: '重命名',
|
||||
label: t('fileTree.rename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'F2',
|
||||
onClick: () => {
|
||||
@@ -644,7 +648,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '批量重命名',
|
||||
label: t('fileTree.batchRename'),
|
||||
icon: <Edit3 size={16} />,
|
||||
shortcut: 'Shift+F2',
|
||||
disabled: true, // TODO: 实现批量重命名
|
||||
@@ -654,7 +658,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '复制',
|
||||
label: t('fileTree.duplicate'),
|
||||
icon: <Clipboard size={16} />,
|
||||
shortcut: 'Ctrl+D',
|
||||
onClick: () => {
|
||||
@@ -664,7 +668,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '删除',
|
||||
label: t('fileTree.delete'),
|
||||
icon: <Trash2 size={16} />,
|
||||
shortcut: 'Delete',
|
||||
onClick: () => handleDeleteClick(node)
|
||||
@@ -672,21 +676,21 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 资产操作子菜单
|
||||
// 资产操作子菜单 | Asset operations submenu
|
||||
items.push({
|
||||
label: '资产操作',
|
||||
label: t('fileTree.assetActions'),
|
||||
icon: <Settings size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: '重新导入',
|
||||
label: t('fileTree.reimport'),
|
||||
icon: <RefreshCw size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Reimport asset:', node.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '导出...',
|
||||
label: t('fileTree.exportAsset'),
|
||||
icon: <Package size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Export asset:', node.path);
|
||||
@@ -694,7 +698,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
},
|
||||
{ label: '', separator: true, onClick: () => {} },
|
||||
{
|
||||
label: '迁移资产',
|
||||
label: t('fileTree.migrateAsset'),
|
||||
icon: <Folder size={16} />,
|
||||
onClick: () => {
|
||||
console.log('Migrate asset:', node.path);
|
||||
@@ -703,26 +707,26 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
]
|
||||
});
|
||||
|
||||
// 资产本地化子菜单
|
||||
// 资产本地化子菜单 | Asset localization submenu
|
||||
items.push({
|
||||
label: '资产本地化',
|
||||
label: t('fileTree.assetLocalization'),
|
||||
icon: <Globe size={16} />,
|
||||
onClick: () => {},
|
||||
children: [
|
||||
{
|
||||
label: '创建本地化资产',
|
||||
label: t('fileTree.createLocalizedAsset'),
|
||||
onClick: () => {
|
||||
console.log('Create localized asset:', node.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '导入翻译',
|
||||
label: t('fileTree.importTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Import translation:', node.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '导出翻译',
|
||||
label: t('fileTree.exportTranslation'),
|
||||
onClick: () => {
|
||||
console.log('Export translation:', node.path);
|
||||
}
|
||||
@@ -732,9 +736,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 标签和引用
|
||||
// 标签和引用 | Tags and references
|
||||
items.push({
|
||||
label: '管理标签',
|
||||
label: t('fileTree.manageTags'),
|
||||
icon: <Tag size={16} />,
|
||||
shortcut: 'Ctrl+T',
|
||||
onClick: () => {
|
||||
@@ -744,9 +748,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 路径复制选项
|
||||
// 路径复制选项 | Path copy options
|
||||
items.push({
|
||||
label: '复制引用',
|
||||
label: t('fileTree.copyReference'),
|
||||
icon: <Link size={16} />,
|
||||
shortcut: 'Ctrl+C',
|
||||
onClick: () => {
|
||||
@@ -755,22 +759,22 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '拷贝Object路径',
|
||||
label: t('fileTree.copyObjectPath'),
|
||||
icon: <Copy size={16} />,
|
||||
shortcut: 'Ctrl+Shift+C',
|
||||
onClick: () => {
|
||||
// 生成对象路径格式
|
||||
// 生成对象路径格式 | Generate object path format
|
||||
const objectPath = node.path.replace(/\\/g, '/');
|
||||
navigator.clipboard.writeText(objectPath);
|
||||
}
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '拷贝包路径',
|
||||
label: t('fileTree.copyPackagePath'),
|
||||
icon: <Package size={16} />,
|
||||
shortcut: 'Ctrl+Alt+C',
|
||||
onClick: () => {
|
||||
// 生成包路径格式
|
||||
// 生成包路径格式 | Generate package path format
|
||||
const packagePath = '/' + node.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
|
||||
navigator.clipboard.writeText(packagePath);
|
||||
}
|
||||
@@ -778,9 +782,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
items.push({ label: '', separator: true, onClick: () => {} });
|
||||
|
||||
// 引用查看器
|
||||
// 引用查看器 | Reference viewer
|
||||
items.push({
|
||||
label: '引用查看器',
|
||||
label: t('fileTree.referenceViewer'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+R',
|
||||
onClick: () => {
|
||||
@@ -789,7 +793,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '尺寸信息图',
|
||||
label: t('fileTree.sizeMap'),
|
||||
icon: <FileSearch size={16} />,
|
||||
shortcut: 'Alt+Shift+D',
|
||||
onClick: () => {
|
||||
@@ -801,13 +805,13 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
|
||||
if (node.type === 'folder') {
|
||||
items.push({
|
||||
label: '新建文件',
|
||||
label: t('fileTree.newFile'),
|
||||
icon: <FileText size={16} />,
|
||||
onClick: () => handleCreateFileClick(node.path)
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: '新建文件夹',
|
||||
label: t('fileTree.newFolder'),
|
||||
icon: <FolderPlus size={16} />,
|
||||
onClick: () => handleCreateFolderClick(node.path)
|
||||
});
|
||||
@@ -830,7 +834,7 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
}
|
||||
|
||||
items.push({
|
||||
label: '在文件管理器中显示',
|
||||
label: t('fileTree.showInExplorer'),
|
||||
icon: <FolderOpen size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -1070,11 +1074,11 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="file-tree loading">Loading...</div>;
|
||||
return <div className="file-tree loading">{t('fileTree.loading')}</div>;
|
||||
}
|
||||
|
||||
if (!rootPath || tree.length === 0) {
|
||||
return <div className="file-tree empty">No folders</div>;
|
||||
return <div className="file-tree empty">{t('fileTree.noFolders')}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -1099,14 +1103,14 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
)}
|
||||
{deleteDialog && (
|
||||
<ConfirmDialog
|
||||
title="确认删除"
|
||||
title={t('fileTree.confirmDelete')}
|
||||
message={
|
||||
deleteDialog.node.type === 'folder'
|
||||
? `确定要删除文件夹 "${deleteDialog.node.name}" 及其所有内容吗?\n此操作无法撤销。`
|
||||
: `确定要删除文件 "${deleteDialog.node.name}" 吗?\n此操作无法撤销。`
|
||||
? t('fileTree.confirmDeleteFolder', { name: deleteDialog.node.name })
|
||||
: t('fileTree.confirmDeleteFile', { name: deleteDialog.node.name })
|
||||
}
|
||||
confirmText="删除"
|
||||
cancelText="取消"
|
||||
confirmText={t('fileTree.delete')}
|
||||
cancelText={t('fileTree.cancel')}
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={() => setDeleteDialog(null)}
|
||||
/>
|
||||
@@ -1114,22 +1118,22 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
{promptDialog && (
|
||||
<PromptDialog
|
||||
title={
|
||||
promptDialog.type === 'create-file' ? '新建文件' :
|
||||
promptDialog.type === 'create-folder' ? '新建文件夹' :
|
||||
'新建文件'
|
||||
promptDialog.type === 'create-file' ? t('fileTree.newFileTitle') :
|
||||
promptDialog.type === 'create-folder' ? t('fileTree.newFolderTitle') :
|
||||
t('fileTree.newFileTitle')
|
||||
}
|
||||
message={
|
||||
promptDialog.type === 'create-file' ? '请输入文件名:' :
|
||||
promptDialog.type === 'create-folder' ? '请输入文件夹名:' :
|
||||
`请输入文件名 (将自动添加 .${promptDialog.templateExtension} 扩展名):`
|
||||
promptDialog.type === 'create-file' ? t('fileTree.enterFileName') :
|
||||
promptDialog.type === 'create-folder' ? t('fileTree.enterFolderName') :
|
||||
t('fileTree.enterTemplateFileName', { ext: promptDialog.templateExtension || '' })
|
||||
}
|
||||
placeholder={
|
||||
promptDialog.type === 'create-file' ? '例如: config.json' :
|
||||
promptDialog.type === 'create-folder' ? '例如: assets' :
|
||||
'例如: MyFile'
|
||||
promptDialog.type === 'create-file' ? t('fileTree.fileNamePlaceholder') :
|
||||
promptDialog.type === 'create-folder' ? t('fileTree.folderNamePlaceholder') :
|
||||
t('fileTree.templateNamePlaceholder')
|
||||
}
|
||||
confirmText="创建"
|
||||
cancelText="取消"
|
||||
confirmText={t('fileTree.create')}
|
||||
cancelText={t('fileTree.cancel')}
|
||||
onConfirm={handlePromptConfirm}
|
||||
onCancel={() => setPromptDialog(null)}
|
||||
/>
|
||||
|
||||
@@ -2,15 +2,16 @@ import { useState } from 'react';
|
||||
import { Github, AlertCircle, CheckCircle, Loader, ExternalLink } from 'lucide-react';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/GitHubAuth.css';
|
||||
|
||||
interface GitHubAuthProps {
|
||||
githubService: GitHubService;
|
||||
onSuccess: () => void;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps) {
|
||||
export function GitHubAuth({ githubService, onSuccess }: GitHubAuthProps) {
|
||||
const { t } = useLocale();
|
||||
const [useOAuth, setUseOAuth] = useState(true);
|
||||
const [githubToken, setGithubToken] = useState('');
|
||||
const [userCode, setUserCode] = useState('');
|
||||
@@ -18,54 +19,6 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
const [authStatus, setAuthStatus] = useState<'idle' | 'pending' | 'authorized' | 'error'>('idle');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
githubLogin: 'GitHub 登录',
|
||||
oauthLogin: 'OAuth 登录(推荐)',
|
||||
tokenLogin: 'Token 登录',
|
||||
oauthStep1: '1. 点击"开始授权"按钮',
|
||||
oauthStep2: '2. 在浏览器中打开 GitHub 授权页面',
|
||||
oauthStep3: '3. 输入下方显示的代码并授权',
|
||||
startAuth: '开始授权',
|
||||
authorizing: '等待授权中...',
|
||||
authorized: '授权成功!',
|
||||
authFailed: '授权失败',
|
||||
userCode: '授权码',
|
||||
copyCode: '复制代码',
|
||||
openBrowser: '打开浏览器',
|
||||
tokenLabel: 'GitHub Personal Access Token',
|
||||
tokenPlaceholder: '粘贴你的 GitHub Token',
|
||||
tokenHint: '需要 repo 和 workflow 权限',
|
||||
createToken: '创建 Token',
|
||||
login: '登录',
|
||||
back: '返回'
|
||||
},
|
||||
en: {
|
||||
githubLogin: 'GitHub Login',
|
||||
oauthLogin: 'OAuth Login (Recommended)',
|
||||
tokenLogin: 'Token Login',
|
||||
oauthStep1: '1. Click "Start Authorization"',
|
||||
oauthStep2: '2. Open GitHub authorization page in browser',
|
||||
oauthStep3: '3. Enter the code shown below and authorize',
|
||||
startAuth: 'Start Authorization',
|
||||
authorizing: 'Waiting for authorization...',
|
||||
authorized: 'Authorized!',
|
||||
authFailed: 'Authorization failed',
|
||||
userCode: 'Authorization Code',
|
||||
copyCode: 'Copy Code',
|
||||
openBrowser: 'Open Browser',
|
||||
tokenLabel: 'GitHub Personal Access Token',
|
||||
tokenPlaceholder: 'Paste your GitHub Token',
|
||||
tokenHint: 'Requires repo and workflow permissions',
|
||||
createToken: 'Create Token',
|
||||
login: 'Login',
|
||||
back: 'Back'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
};
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
setAuthStatus('pending');
|
||||
setError('');
|
||||
@@ -101,7 +54,7 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
|
||||
const handleTokenAuth = async () => {
|
||||
if (!githubToken.trim()) {
|
||||
setError(locale === 'zh' ? '请输入 Token' : 'Please enter a token');
|
||||
setError(t('github.enterToken'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,7 +64,7 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
console.error('[GitHubAuth] Token auth failed:', err);
|
||||
setError(locale === 'zh' ? '认证失败,请检查你的 Token' : 'Authentication failed. Please check your token.');
|
||||
setError(t('github.authFailedToken'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -130,20 +83,20 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
return (
|
||||
<div className="github-auth">
|
||||
<Github size={48} style={{ color: '#0366d6' }} />
|
||||
<p>{t('githubLogin')}</p>
|
||||
<p>{t('github.githubLogin')}</p>
|
||||
|
||||
<div className="auth-tabs">
|
||||
<button
|
||||
className={`auth-tab ${useOAuth ? 'active' : ''}`}
|
||||
onClick={() => setUseOAuth(true)}
|
||||
>
|
||||
{t('oauthLogin')}
|
||||
{t('github.oauthLogin')}
|
||||
</button>
|
||||
<button
|
||||
className={`auth-tab ${!useOAuth ? 'active' : ''}`}
|
||||
onClick={() => setUseOAuth(false)}
|
||||
>
|
||||
{t('tokenLogin')}
|
||||
{t('github.tokenLogin')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -152,14 +105,14 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
{authStatus === 'idle' && (
|
||||
<>
|
||||
<div className="oauth-instructions">
|
||||
<p>{t('oauthStep1')}</p>
|
||||
<p>{t('oauthStep2')}</p>
|
||||
<p>{t('oauthStep3')}</p>
|
||||
<p>{t('github.oauthStep1')}</p>
|
||||
<p>{t('github.oauthStep2')}</p>
|
||||
<p>{t('github.oauthStep3')}</p>
|
||||
</div>
|
||||
|
||||
<button className="btn-primary" onClick={handleOAuthLogin}>
|
||||
<Github size={16} />
|
||||
{t('startAuth')}
|
||||
{t('github.startAuth')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -167,17 +120,17 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
{authStatus === 'pending' && (
|
||||
<div className="oauth-pending">
|
||||
<Loader size={48} className="spinning" style={{ color: '#0366d6' }} />
|
||||
<h4>{t('authorizing')}</h4>
|
||||
<h4>{t('github.authorizing')}</h4>
|
||||
|
||||
{userCode && (
|
||||
<div className="user-code-display">
|
||||
<label>{t('userCode')}</label>
|
||||
<label>{t('github.userCode')}</label>
|
||||
<div className="code-box">
|
||||
<span className="code-text">{userCode}</span>
|
||||
<button
|
||||
className="btn-copy"
|
||||
onClick={() => copyToClipboard(userCode)}
|
||||
title={t('copyCode')}
|
||||
title={t('github.copyCode')}
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
@@ -187,7 +140,7 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
onClick={() => open(verificationUri)}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{t('openBrowser')}
|
||||
{t('github.openBrowser')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -197,21 +150,21 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
{authStatus === 'authorized' && (
|
||||
<div className="oauth-success">
|
||||
<CheckCircle size={48} style={{ color: '#34c759' }} />
|
||||
<h4>{t('authorized')}</h4>
|
||||
<h4>{t('github.authorized')}</h4>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{authStatus === 'error' && (
|
||||
<div className="oauth-error">
|
||||
<AlertCircle size={48} style={{ color: '#ff3b30' }} />
|
||||
<h4>{t('authFailed')}</h4>
|
||||
<h4>{t('github.authFailed')}</h4>
|
||||
{error && (
|
||||
<div className="error-details">
|
||||
<pre>{error}</pre>
|
||||
</div>
|
||||
)}
|
||||
<button className="btn-secondary" onClick={() => setAuthStatus('idle')}>
|
||||
{t('back')}
|
||||
{t('github.back')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -219,28 +172,28 @@ export function GitHubAuth({ githubService, onSuccess, locale }: GitHubAuthProps
|
||||
) : (
|
||||
<div className="token-auth">
|
||||
<div className="form-group">
|
||||
<label>{t('tokenLabel')}</label>
|
||||
<label>{t('github.tokenLabel')}</label>
|
||||
<input
|
||||
type="password"
|
||||
value={githubToken}
|
||||
onChange={(e) => setGithubToken(e.target.value)}
|
||||
placeholder={t('tokenPlaceholder')}
|
||||
placeholder={t('github.tokenPlaceholder')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleTokenAuth();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<small>{t('tokenHint')}</small>
|
||||
<small>{t('github.tokenHint')}</small>
|
||||
</div>
|
||||
|
||||
<button className="btn-link" onClick={openCreateTokenPage}>
|
||||
<ExternalLink size={14} />
|
||||
{t('createToken')}
|
||||
{t('github.createToken')}
|
||||
</button>
|
||||
|
||||
<button className="btn-primary" onClick={handleTokenAuth}>
|
||||
{t('login')}
|
||||
{t('github.login')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,32 +1,22 @@
|
||||
import { X } from 'lucide-react';
|
||||
import { GitHubService } from '../services/GitHubService';
|
||||
import { GitHubAuth } from './GitHubAuth';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/GitHubLoginDialog.css';
|
||||
|
||||
interface GitHubLoginDialogProps {
|
||||
githubService: GitHubService;
|
||||
onClose: () => void;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function GitHubLoginDialog({ githubService, onClose, locale }: GitHubLoginDialogProps) {
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
title: 'GitHub 登录'
|
||||
},
|
||||
en: {
|
||||
title: 'GitHub Login'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
};
|
||||
export function GitHubLoginDialog({ githubService, onClose }: GitHubLoginDialogProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="github-login-overlay" onClick={onClose}>
|
||||
<div className="github-login-dialog" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="github-login-header">
|
||||
<h2>{t('title')}</h2>
|
||||
<h2>{t('github.title')}</h2>
|
||||
<button className="github-login-close" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
@@ -36,7 +26,6 @@ export function GitHubLoginDialog({ githubService, onClose, locale }: GitHubLogi
|
||||
<GitHubAuth
|
||||
githubService={githubService}
|
||||
onSuccess={onClose}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
ChevronDown
|
||||
} from 'lucide-react';
|
||||
import type { MessageHub, CommandManager } from '@esengine/editor-core';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/MainToolbar.css';
|
||||
|
||||
export type PlayState = 'stopped' | 'playing' | 'paused';
|
||||
|
||||
interface MainToolbarProps {
|
||||
locale?: string;
|
||||
messageHub?: MessageHub;
|
||||
commandManager?: CommandManager;
|
||||
onSaveScene?: () => void;
|
||||
@@ -61,7 +61,6 @@ function ToolSeparator() {
|
||||
}
|
||||
|
||||
export function MainToolbar({
|
||||
locale = 'en',
|
||||
messageHub,
|
||||
commandManager,
|
||||
onSaveScene,
|
||||
@@ -75,46 +74,13 @@ export function MainToolbar({
|
||||
onRunInBrowser,
|
||||
onRunOnDevice
|
||||
}: MainToolbarProps) {
|
||||
const { t } = useLocale();
|
||||
const [playState, setPlayState] = useState<PlayState>('stopped');
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [showRunMenu, setShowRunMenu] = useState(false);
|
||||
const runMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
stop: 'Stop',
|
||||
step: 'Step Forward',
|
||||
save: 'Save Scene (Ctrl+S)',
|
||||
open: 'Open Scene',
|
||||
undo: 'Undo (Ctrl+Z)',
|
||||
redo: 'Redo (Ctrl+Y)',
|
||||
preview: 'Preview Mode',
|
||||
runOptions: 'Run Options',
|
||||
runInBrowser: 'Run in Browser',
|
||||
runOnDevice: 'Run on Device'
|
||||
},
|
||||
zh: {
|
||||
play: '播放',
|
||||
pause: '暂停',
|
||||
stop: '停止',
|
||||
step: '单步执行',
|
||||
save: '保存场景 (Ctrl+S)',
|
||||
open: '打开场景',
|
||||
undo: '撤销 (Ctrl+Z)',
|
||||
redo: '重做 (Ctrl+Y)',
|
||||
preview: '预览模式',
|
||||
runOptions: '运行选项',
|
||||
runInBrowser: '浏览器运行',
|
||||
runOnDevice: '真机运行'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
// Close run menu when clicking outside
|
||||
useEffect(() => {
|
||||
if (!showRunMenu) return;
|
||||
@@ -228,12 +194,12 @@ export function MainToolbar({
|
||||
<div className="toolbar-group">
|
||||
<ToolButton
|
||||
icon={<Save size={16} />}
|
||||
label={t('save')}
|
||||
label={t('toolbar.save')}
|
||||
onClick={onSaveScene}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<FolderOpen size={16} />}
|
||||
label={t('open')}
|
||||
label={t('toolbar.open')}
|
||||
onClick={onOpenScene}
|
||||
/>
|
||||
</div>
|
||||
@@ -244,13 +210,13 @@ export function MainToolbar({
|
||||
<div className="toolbar-group">
|
||||
<ToolButton
|
||||
icon={<Undo2 size={16} />}
|
||||
label={t('undo')}
|
||||
label={t('toolbar.undo')}
|
||||
disabled={!canUndo}
|
||||
onClick={handleUndo}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<Redo2 size={16} />}
|
||||
label={t('redo')}
|
||||
label={t('toolbar.redo')}
|
||||
disabled={!canRedo}
|
||||
onClick={handleRedo}
|
||||
/>
|
||||
@@ -261,18 +227,18 @@ export function MainToolbar({
|
||||
<div className="toolbar-group toolbar-center">
|
||||
<ToolButton
|
||||
icon={playState === 'playing' ? <Pause size={18} /> : <Play size={18} />}
|
||||
label={playState === 'playing' ? t('pause') : t('play')}
|
||||
label={playState === 'playing' ? t('toolbar.pause') : t('toolbar.play')}
|
||||
onClick={playState === 'playing' ? handlePause : handlePlay}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<Square size={16} />}
|
||||
label={t('stop')}
|
||||
label={t('toolbar.stop')}
|
||||
disabled={playState === 'stopped'}
|
||||
onClick={handleStop}
|
||||
/>
|
||||
<ToolButton
|
||||
icon={<SkipForward size={16} />}
|
||||
label={t('step')}
|
||||
label={t('toolbar.step')}
|
||||
disabled={playState === 'playing'}
|
||||
onClick={handleStep}
|
||||
/>
|
||||
@@ -287,7 +253,7 @@ export function MainToolbar({
|
||||
e.stopPropagation();
|
||||
setShowRunMenu(prev => !prev);
|
||||
}}
|
||||
title={t('runOptions')}
|
||||
title={t('toolbar.runOptions')}
|
||||
type="button"
|
||||
>
|
||||
<Globe size={16} />
|
||||
@@ -297,11 +263,11 @@ export function MainToolbar({
|
||||
<div className="toolbar-dropdown-menu">
|
||||
<button type="button" onClick={handleRunInBrowser}>
|
||||
<Globe size={14} />
|
||||
<span>{t('runInBrowser')}</span>
|
||||
<span>{t('toolbar.runInBrowser')}</span>
|
||||
</button>
|
||||
<button type="button" onClick={handleRunOnDevice}>
|
||||
<QrCode size={14} />
|
||||
<span>{t('runOnDevice')}</span>
|
||||
<span>{t('toolbar.runOnDevice')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -314,7 +280,7 @@ export function MainToolbar({
|
||||
{playState !== 'stopped' && (
|
||||
<div className="preview-indicator">
|
||||
<Eye size={14} />
|
||||
<span>{t('preview')}</span>
|
||||
<span>{t('toolbar.preview')}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect } from 'react';
|
||||
import { UIRegistry, MessageHub, PluginManager } from '@esengine/editor-core';
|
||||
import type { MenuItem as PluginMenuItem } from '@esengine/editor-core';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/MenuBar.css';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -15,7 +16,6 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
interface MenuBarProps {
|
||||
locale?: string;
|
||||
uiRegistry?: UIRegistry;
|
||||
messageHub?: MessageHub;
|
||||
pluginManager?: PluginManager;
|
||||
@@ -38,7 +38,6 @@ interface MenuBarProps {
|
||||
}
|
||||
|
||||
export function MenuBar({
|
||||
locale = 'en',
|
||||
uiRegistry,
|
||||
messageHub,
|
||||
pluginManager,
|
||||
@@ -59,6 +58,7 @@ export function MenuBar({
|
||||
onReloadPlugins,
|
||||
onOpenBuildSettings
|
||||
}: MenuBarProps) {
|
||||
const { t } = useLocale();
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -96,109 +96,31 @@ export function MenuBar({
|
||||
}
|
||||
}, [messageHub, uiRegistry, pluginManager]);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
file: 'File',
|
||||
newScene: 'New Scene',
|
||||
openScene: 'Open Scene',
|
||||
saveScene: 'Save Scene',
|
||||
saveSceneAs: 'Save Scene As...',
|
||||
openProject: 'Open Project',
|
||||
closeProject: 'Close Project',
|
||||
exit: 'Exit',
|
||||
edit: 'Edit',
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
cut: 'Cut',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
delete: 'Delete',
|
||||
selectAll: 'Select All',
|
||||
window: 'Window',
|
||||
sceneHierarchy: 'Scene Hierarchy',
|
||||
inspector: 'Inspector',
|
||||
assets: 'Assets',
|
||||
console: 'Console',
|
||||
viewport: 'Viewport',
|
||||
pluginManager: 'Plugin Manager',
|
||||
tools: 'Tools',
|
||||
createPlugin: 'Create Plugin',
|
||||
reloadPlugins: 'Reload Plugins',
|
||||
portManager: 'Port Manager',
|
||||
settings: 'Settings',
|
||||
help: 'Help',
|
||||
documentation: 'Documentation',
|
||||
checkForUpdates: 'Check for Updates',
|
||||
about: 'About',
|
||||
devtools: 'Developer Tools',
|
||||
buildSettings: 'Build Settings'
|
||||
},
|
||||
zh: {
|
||||
file: '文件',
|
||||
newScene: '新建场景',
|
||||
openScene: '打开场景',
|
||||
saveScene: '保存场景',
|
||||
saveSceneAs: '场景另存为...',
|
||||
openProject: '打开项目',
|
||||
closeProject: '关闭项目',
|
||||
exit: '退出',
|
||||
edit: '编辑',
|
||||
undo: '撤销',
|
||||
redo: '重做',
|
||||
cut: '剪切',
|
||||
copy: '复制',
|
||||
paste: '粘贴',
|
||||
delete: '删除',
|
||||
selectAll: '全选',
|
||||
window: '窗口',
|
||||
sceneHierarchy: '场景层级',
|
||||
inspector: '检视器',
|
||||
assets: '资产',
|
||||
console: '控制台',
|
||||
viewport: '视口',
|
||||
pluginManager: '插件管理器',
|
||||
tools: '工具',
|
||||
createPlugin: '创建插件',
|
||||
reloadPlugins: '重新加载插件',
|
||||
portManager: '端口管理器',
|
||||
settings: '设置',
|
||||
help: '帮助',
|
||||
documentation: '文档',
|
||||
checkForUpdates: '检查更新',
|
||||
about: '关于',
|
||||
devtools: '开发者工具',
|
||||
buildSettings: '构建设置'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
const menus: Record<string, MenuItem[]> = {
|
||||
file: [
|
||||
{ label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ label: t('menu.file.newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('menu.file.openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ separator: true },
|
||||
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ label: t('menu.file.saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('menu.file.saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ separator: true },
|
||||
{ label: t('buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ label: t('menu.file.buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ separator: true },
|
||||
{ label: t('openProject'), onClick: onOpenProject },
|
||||
{ label: t('closeProject'), onClick: onCloseProject },
|
||||
{ label: t('menu.file.openProject'), onClick: onOpenProject },
|
||||
{ label: t('menu.file.closeProject'), onClick: onCloseProject },
|
||||
{ separator: true },
|
||||
{ label: t('exit'), onClick: onExit }
|
||||
{ label: t('menu.file.exit'), onClick: onExit }
|
||||
],
|
||||
edit: [
|
||||
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ label: t('menu.edit.undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('menu.edit.redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('delete'), shortcut: 'Delete', disabled: true },
|
||||
{ label: t('menu.edit.cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('menu.edit.copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('menu.edit.paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('menu.edit.delete'), shortcut: 'Delete', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
{ label: t('menu.edit.selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
],
|
||||
window: [
|
||||
...pluginMenuItems.map((item) => ({
|
||||
@@ -208,25 +130,34 @@ export function MenuBar({
|
||||
onClick: item.onClick
|
||||
})),
|
||||
...(pluginMenuItems.length > 0 ? [{ separator: true } as MenuItem] : []),
|
||||
{ label: t('pluginManager'), onClick: onOpenPluginManager },
|
||||
{ label: t('menu.window.pluginManager'), onClick: onOpenPluginManager },
|
||||
{ separator: true },
|
||||
{ label: t('devtools'), onClick: onToggleDevtools }
|
||||
{ label: t('menu.window.devtools'), onClick: onToggleDevtools }
|
||||
],
|
||||
tools: [
|
||||
{ label: t('createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ label: t('menu.tools.createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('menu.tools.reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ separator: true },
|
||||
{ label: t('portManager'), onClick: onOpenPortManager },
|
||||
{ label: t('menu.tools.portManager'), onClick: onOpenPortManager },
|
||||
{ separator: true },
|
||||
{ label: t('settings'), onClick: onOpenSettings }
|
||||
{ label: t('menu.tools.settings'), onClick: onOpenSettings }
|
||||
],
|
||||
help: [
|
||||
{ label: t('documentation'), disabled: true },
|
||||
{ label: t('menu.help.documentation'), disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('about'), onClick: onOpenAbout }
|
||||
{ label: t('menu.help.about'), onClick: onOpenAbout }
|
||||
]
|
||||
};
|
||||
|
||||
// 菜单键到翻译键的映射 | Map menu keys to translation keys
|
||||
const menuTitleKeys: Record<string, string> = {
|
||||
file: 'menu.file.title',
|
||||
edit: 'menu.edit.title',
|
||||
window: 'menu.window.title',
|
||||
tools: 'menu.tools.title',
|
||||
help: 'menu.help.title'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
@@ -259,7 +190,7 @@ export function MenuBar({
|
||||
className={`menu-button ${openMenu === menuKey ? 'active' : ''}`}
|
||||
onClick={() => handleMenuClick(menuKey)}
|
||||
>
|
||||
{t(menuKey)}
|
||||
{t(menuTitleKeys[menuKey] || menuKey)}
|
||||
</button>
|
||||
{openMenu === menuKey && menus[menuKey] && (
|
||||
<div className="menu-dropdown">
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Search, Filter, Settings, X, Trash2, ChevronDown,
|
||||
Bug, Info, AlertTriangle, XCircle, AlertCircle, Wifi, Pause, Play, Copy
|
||||
} from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/OutputLogPanel.css';
|
||||
|
||||
interface OutputLogPanelProps {
|
||||
@@ -145,6 +146,7 @@ const LogEntryItem = memo(({ log, isExpanded, onToggle, onCopy }: {
|
||||
LogEntryItem.displayName = 'LogEntryItem';
|
||||
|
||||
export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLogPanelProps) {
|
||||
const { t } = useLocale();
|
||||
const [logs, setLogs] = useState<LogEntry[]>(() => logService.getLogs().slice(-MAX_LOGS));
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [levelFilter, setLevelFilter] = useState<Set<LogLevel>>(new Set([
|
||||
@@ -279,7 +281,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={locale === 'zh' ? '搜索日志...' : 'Search logs...'}
|
||||
placeholder={t('outputLog.searchPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -305,7 +307,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
}}
|
||||
>
|
||||
<Filter size={14} />
|
||||
<span>{locale === 'zh' ? '过滤器' : 'Filters'}</span>
|
||||
<span>{t('outputLog.filters')}</span>
|
||||
{activeFilterCount > 0 && (
|
||||
<span className="filter-badge">{activeFilterCount}</span>
|
||||
)}
|
||||
@@ -314,7 +316,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
{showFilterMenu && (
|
||||
<div className="output-log-menu">
|
||||
<div className="output-log-menu-header">
|
||||
{locale === 'zh' ? '日志级别' : 'Log Levels'}
|
||||
{t('outputLog.logLevels')}
|
||||
</div>
|
||||
<label className="output-log-menu-item">
|
||||
<input
|
||||
@@ -364,7 +366,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
onChange={() => setShowRemoteOnly(!showRemoteOnly)}
|
||||
/>
|
||||
<Wifi size={14} className="level-icon remote" />
|
||||
<span>{locale === 'zh' ? '仅远程日志' : 'Remote Only'}</span>
|
||||
<span>{t('outputLog.remoteOnly')}</span>
|
||||
<span className="level-count">{remoteLogCount}</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -376,8 +378,8 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
className={`output-log-icon-btn ${autoScroll ? 'active' : ''}`}
|
||||
onClick={() => setAutoScroll(!autoScroll)}
|
||||
title={autoScroll
|
||||
? (locale === 'zh' ? '暂停自动滚动' : 'Pause auto-scroll')
|
||||
: (locale === 'zh' ? '恢复自动滚动' : 'Resume auto-scroll')
|
||||
? t('outputLog.pauseAutoScroll')
|
||||
: t('outputLog.resumeAutoScroll')
|
||||
}
|
||||
>
|
||||
{autoScroll ? <Pause size={14} /> : <Play size={14} />}
|
||||
@@ -391,7 +393,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
setShowSettingsMenu(!showSettingsMenu);
|
||||
setShowFilterMenu(false);
|
||||
}}
|
||||
title={locale === 'zh' ? '设置' : 'Settings'}
|
||||
title={t('outputLog.settings')}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
@@ -402,7 +404,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
onClick={handleClear}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>{locale === 'zh' ? '清空日志' : 'Clear Logs'}</span>
|
||||
<span>{t('outputLog.clearLogs')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -430,8 +432,8 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
<div className="output-log-empty">
|
||||
<AlertCircle size={32} />
|
||||
<p>{searchQuery
|
||||
? (locale === 'zh' ? '没有匹配的日志' : 'No matching logs')
|
||||
: (locale === 'zh' ? '暂无日志' : 'No logs to display')
|
||||
? t('outputLog.noMatchingLogs')
|
||||
: t('outputLog.noLogs')
|
||||
}</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -449,7 +451,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="output-log-status">
|
||||
<span>{filteredLogs.length} / {logs.length} {locale === 'zh' ? '条日志' : 'logs'}</span>
|
||||
<span>{filteredLogs.length} / {logs.length} {t('outputLog.logs')}</span>
|
||||
{!autoScroll && (
|
||||
<button
|
||||
className="output-log-scroll-btn"
|
||||
@@ -460,7 +462,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog
|
||||
}
|
||||
}}
|
||||
>
|
||||
↓ {locale === 'zh' ? '滚动到底部' : 'Scroll to bottom'}
|
||||
↓ {t('outputLog.scrollToBottom')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { useState } from 'react';
|
||||
import { X, FolderOpen } from 'lucide-react';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/PluginGeneratorWindow.css';
|
||||
|
||||
interface PluginGeneratorWindowProps {
|
||||
onClose: () => void;
|
||||
projectPath: string | null;
|
||||
locale: string;
|
||||
onSuccess?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess }: PluginGeneratorWindowProps) {
|
||||
export function PluginGeneratorWindow({ onClose, projectPath, onSuccess }: PluginGeneratorWindowProps) {
|
||||
const { t } = useLocale();
|
||||
const [pluginName, setPluginName] = useState('');
|
||||
const [pluginVersion, setPluginVersion] = useState('1.0.0');
|
||||
const [outputPath, setOutputPath] = useState(projectPath ? `${projectPath}/plugins` : '');
|
||||
@@ -18,44 +19,6 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
zh: {
|
||||
title: '创建插件',
|
||||
pluginName: '插件名称',
|
||||
pluginNamePlaceholder: '例如: my-game-plugin',
|
||||
pluginVersion: '插件版本',
|
||||
outputPath: '输出路径',
|
||||
selectPath: '选择路径',
|
||||
includeExample: '包含示例节点',
|
||||
generate: '生成插件',
|
||||
cancel: '取消',
|
||||
generating: '正在生成...',
|
||||
success: '插件创建成功!',
|
||||
errorEmpty: '请输入插件名称',
|
||||
errorInvalidName: '插件名称只能包含字母、数字、连字符和下划线',
|
||||
errorNoPath: '请选择输出路径'
|
||||
},
|
||||
en: {
|
||||
title: 'Create Plugin',
|
||||
pluginName: 'Plugin Name',
|
||||
pluginNamePlaceholder: 'e.g: my-game-plugin',
|
||||
pluginVersion: 'Plugin Version',
|
||||
outputPath: 'Output Path',
|
||||
selectPath: 'Select Path',
|
||||
includeExample: 'Include Example Node',
|
||||
generate: 'Generate Plugin',
|
||||
cancel: 'Cancel',
|
||||
generating: 'Generating...',
|
||||
success: 'Plugin created successfully!',
|
||||
errorEmpty: 'Please enter plugin name',
|
||||
errorInvalidName: 'Plugin name can only contain letters, numbers, hyphens and underscores',
|
||||
errorNoPath: 'Please select output path'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || translations.en?.[key] || key;
|
||||
};
|
||||
|
||||
const handleSelectPath = async () => {
|
||||
try {
|
||||
const selected = await TauriAPI.openProjectDialog();
|
||||
@@ -69,11 +32,11 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
|
||||
const validatePluginName = (name: string): boolean => {
|
||||
if (!name) {
|
||||
setError(t('errorEmpty'));
|
||||
setError(t('pluginGenerator.errorEmpty'));
|
||||
return false;
|
||||
}
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(name)) {
|
||||
setError(t('errorInvalidName'));
|
||||
setError(t('pluginGenerator.errorInvalidName'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -87,7 +50,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
}
|
||||
|
||||
if (!outputPath) {
|
||||
setError(t('errorNoPath'));
|
||||
setError(t('pluginGenerator.errorNoPath'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,7 +77,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
alert(t('success'));
|
||||
alert(t('pluginGenerator.success'));
|
||||
|
||||
if (result.path) {
|
||||
await TauriAPI.showInFolder(result.path);
|
||||
@@ -137,7 +100,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content plugin-generator-window">
|
||||
<div className="modal-header">
|
||||
<h2>{t('title')}</h2>
|
||||
<h2>{t('pluginGenerator.title')}</h2>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
@@ -145,18 +108,18 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="form-group">
|
||||
<label>{t('pluginName')}</label>
|
||||
<label>{t('pluginGenerator.pluginName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pluginName}
|
||||
onChange={(e) => setPluginName(e.target.value)}
|
||||
placeholder={t('pluginNamePlaceholder')}
|
||||
placeholder={t('pluginGenerator.pluginNamePlaceholder')}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t('pluginVersion')}</label>
|
||||
<label>{t('pluginGenerator.pluginVersion')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={pluginVersion}
|
||||
@@ -166,7 +129,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t('outputPath')}</label>
|
||||
<label>{t('pluginGenerator.outputPath')}</label>
|
||||
<div className="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
@@ -180,7 +143,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
disabled={isGenerating}
|
||||
>
|
||||
<FolderOpen size={16} />
|
||||
{t('selectPath')}
|
||||
{t('pluginGenerator.selectPath')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,7 +156,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
onChange={(e) => setIncludeExample(e.target.checked)}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<span>{t('includeExample')}</span>
|
||||
<span>{t('pluginGenerator.includeExample')}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -210,14 +173,14 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{isGenerating ? t('generating') : t('generate')}
|
||||
{isGenerating ? t('pluginGenerator.generating') : t('pluginGenerator.generate')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={onClose}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t('pluginGenerator.cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import { Folder, Sparkles, X } from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/ProjectCreationWizard.css';
|
||||
|
||||
// 项目模板类型
|
||||
// 项目模板类型(使用翻译键)
|
||||
// Project template type (using translation keys)
|
||||
interface ProjectTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
nameZh: string;
|
||||
description: string;
|
||||
descriptionZh: string;
|
||||
nameKey: string; // 翻译键 | Translation key
|
||||
descriptionKey: string;
|
||||
}
|
||||
|
||||
const templates: ProjectTemplate[] = [
|
||||
{
|
||||
id: 'blank',
|
||||
name: 'Blank',
|
||||
nameZh: '空白',
|
||||
description: 'A blank project with no starter content. Perfect for starting from scratch.',
|
||||
descriptionZh: '不包含任何启动内容的空白项目,适合从零开始创建。'
|
||||
nameKey: 'project.wizard.templates.blank',
|
||||
descriptionKey: 'project.wizard.templates.blankDesc'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -34,30 +32,16 @@ export function ProjectCreationWizard({
|
||||
onClose,
|
||||
onCreateProject,
|
||||
onBrowsePath,
|
||||
locale
|
||||
locale: _locale
|
||||
}: ProjectCreationWizardProps) {
|
||||
const { t } = useLocale();
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('blank');
|
||||
const [projectName, setProjectName] = useState('MyProject');
|
||||
const [projectPath, setProjectPath] = useState('');
|
||||
|
||||
const t = {
|
||||
title: locale === 'zh' ? '项目浏览器' : 'Project Browser',
|
||||
recentProjects: locale === 'zh' ? '最近打开的项目' : 'Recent Projects',
|
||||
newProject: locale === 'zh' ? '新建项目' : 'New Project',
|
||||
projectName: locale === 'zh' ? '项目名称' : 'Project Name',
|
||||
projectLocation: locale === 'zh' ? '项目位置' : 'Project Location',
|
||||
browse: locale === 'zh' ? '浏览...' : 'Browse...',
|
||||
create: locale === 'zh' ? '创建' : 'Create',
|
||||
cancel: locale === 'zh' ? '取消' : 'Cancel',
|
||||
selectTemplate: locale === 'zh' ? '选择模板' : 'Select a Template',
|
||||
projectSettings: locale === 'zh' ? '项目设置' : 'Project Settings',
|
||||
blank: locale === 'zh' ? '空白' : 'Blank',
|
||||
blankDesc: locale === 'zh' ? '不含任何代码的空白项目。' : 'A blank project with no code.'
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const currentTemplate = templates.find(t => t.id === selectedTemplate);
|
||||
const currentTemplate = templates.find(tmpl => tmpl.id === selectedTemplate);
|
||||
|
||||
const handleBrowse = async () => {
|
||||
const path = await onBrowsePath();
|
||||
@@ -77,7 +61,7 @@ export function ProjectCreationWizard({
|
||||
<div className="project-wizard-overlay">
|
||||
<div className="project-wizard">
|
||||
<div className="wizard-header">
|
||||
<h1>{t.title}</h1>
|
||||
<h1>{t('project.wizard.title')}</h1>
|
||||
<button className="wizard-close-btn" onClick={onClose}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
@@ -87,7 +71,7 @@ export function ProjectCreationWizard({
|
||||
{/* Templates grid */}
|
||||
<div className="wizard-templates">
|
||||
<div className="templates-header">
|
||||
<h3>{t.selectTemplate}</h3>
|
||||
<h3>{t('project.wizard.selectTemplate')}</h3>
|
||||
</div>
|
||||
<div className="templates-grid">
|
||||
{templates.map(template => (
|
||||
@@ -100,7 +84,7 @@ export function ProjectCreationWizard({
|
||||
<Sparkles size={32} />
|
||||
</div>
|
||||
<div className="template-name">
|
||||
{locale === 'zh' ? template.nameZh : template.name}
|
||||
{t(template.nameKey)}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
@@ -116,15 +100,15 @@ export function ProjectCreationWizard({
|
||||
</div>
|
||||
|
||||
<div className="details-info">
|
||||
<h2>{locale === 'zh' ? currentTemplate?.nameZh : currentTemplate?.name}</h2>
|
||||
<p>{locale === 'zh' ? currentTemplate?.descriptionZh : currentTemplate?.description}</p>
|
||||
<h2>{currentTemplate ? t(currentTemplate.nameKey) : ''}</h2>
|
||||
<p>{currentTemplate ? t(currentTemplate.descriptionKey) : ''}</p>
|
||||
</div>
|
||||
|
||||
<div className="details-settings">
|
||||
<h3>{t.projectSettings}</h3>
|
||||
<h3>{t('project.wizard.projectSettings')}</h3>
|
||||
|
||||
<div className="setting-field">
|
||||
<label>{t.projectName}</label>
|
||||
<label>{t('project.wizard.projectName')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={projectName}
|
||||
@@ -134,7 +118,7 @@ export function ProjectCreationWizard({
|
||||
</div>
|
||||
|
||||
<div className="setting-field">
|
||||
<label>{t.projectLocation}</label>
|
||||
<label>{t('project.wizard.projectLocation')}</label>
|
||||
<div className="path-input-group">
|
||||
<input
|
||||
type="text"
|
||||
@@ -153,14 +137,14 @@ export function ProjectCreationWizard({
|
||||
|
||||
<div className="wizard-footer">
|
||||
<button className="wizard-btn secondary" onClick={onClose}>
|
||||
{t.cancel}
|
||||
{t('project.wizard.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="wizard-btn primary"
|
||||
onClick={handleCreate}
|
||||
disabled={!projectName || !projectPath}
|
||||
>
|
||||
{t.create}
|
||||
{t('project.wizard.create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -480,11 +480,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
if (!entity) return;
|
||||
|
||||
const confirmed = await confirm(
|
||||
locale === 'zh'
|
||||
? `确定要删除实体 "${entity.name}" 吗?`
|
||||
: `Are you sure you want to delete entity "${entity.name}"?`,
|
||||
t('hierarchy.deleteConfirm', { name: entity.name }),
|
||||
{
|
||||
title: locale === 'zh' ? '删除实体' : 'Delete Entity',
|
||||
title: t('hierarchy.deleteEntity'),
|
||||
kind: 'warning'
|
||||
}
|
||||
);
|
||||
@@ -544,7 +542,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
*/
|
||||
const handleCreateFolder = useCallback(() => {
|
||||
const entityCount = entityStore.getAllEntities().length;
|
||||
const folderName = locale === 'zh' ? `文件夹 ${entityCount + 1}` : `Folder ${entityCount + 1}`;
|
||||
const folderName = `Folder ${entityCount + 1}`;
|
||||
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
@@ -656,7 +654,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={locale === 'zh' ? '搜索...' : 'Search...'}
|
||||
placeholder={t('hierarchy.search')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
@@ -669,14 +667,14 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<button
|
||||
className="outliner-action-btn"
|
||||
onClick={handleCreateEntity}
|
||||
title={locale === 'zh' ? '创建实体' : 'Create Entity'}
|
||||
title={t('hierarchy.createEntity')}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="outliner-action-btn"
|
||||
onClick={handleCreateFolder}
|
||||
title={locale === 'zh' ? '创建文件夹' : 'Create Folder'}
|
||||
title={t('hierarchy.createFolder')}
|
||||
>
|
||||
<FolderPlus size={14} />
|
||||
</button>
|
||||
@@ -684,7 +682,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
)}
|
||||
<button
|
||||
className="outliner-action-btn"
|
||||
title={locale === 'zh' ? '设置' : 'Settings'}
|
||||
title={t('hierarchy.settings')}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
@@ -695,14 +693,14 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<button
|
||||
className={`mode-btn ${viewMode === 'local' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('local')}
|
||||
title={locale === 'zh' ? '本地场景' : 'Local Scene'}
|
||||
title={t('hierarchy.localScene')}
|
||||
>
|
||||
<Monitor size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`mode-btn ${viewMode === 'remote' ? 'active' : ''}`}
|
||||
onClick={() => setViewMode('remote')}
|
||||
title={locale === 'zh' ? '远程实体' : 'Remote Entities'}
|
||||
title={t('hierarchy.remoteEntities')}
|
||||
>
|
||||
<Globe size={14} />
|
||||
</button>
|
||||
@@ -719,9 +717,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
{/* Column Headers */}
|
||||
<div className="outliner-header">
|
||||
<div className="outliner-header-icons">
|
||||
<span title={locale === 'zh' ? '可见性' : 'Visibility'}><Eye size={12} className="header-icon" /></span>
|
||||
<span title={locale === 'zh' ? '收藏' : 'Favorite'}><Star size={12} className="header-icon" /></span>
|
||||
<span title={locale === 'zh' ? '锁定' : 'Lock'}><Lock size={12} className="header-icon" /></span>
|
||||
<span title={t('hierarchy.visibility')}><Eye size={12} className="header-icon" /></span>
|
||||
<span title={t('hierarchy.favorite')}><Star size={12} className="header-icon" /></span>
|
||||
<span title={t('hierarchy.lock')}><Lock size={12} className="header-icon" /></span>
|
||||
</div>
|
||||
<div
|
||||
className={`outliner-header-label ${sortColumn === 'name' ? 'sorted' : ''}`}
|
||||
@@ -751,7 +749,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<div className="empty-state">
|
||||
<Box size={32} strokeWidth={1.5} className="empty-icon" />
|
||||
<div className="empty-hint">
|
||||
{locale === 'zh' ? '远程游戏中没有实体' : 'No entities in remote game'}
|
||||
{t('hierarchy.remoteEmpty')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -783,7 +781,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
<div className="empty-state">
|
||||
<Box size={32} strokeWidth={1.5} className="empty-icon" />
|
||||
<div className="empty-hint">
|
||||
{locale === 'zh' ? '创建实体开始使用' : 'Create an entity to get started'}
|
||||
{t('hierarchy.emptyHint')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -873,9 +871,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
|
||||
{/* Status Bar */}
|
||||
<div className="outliner-status">
|
||||
<span>{totalCount} {locale === 'zh' ? '个对象' : 'actors'}</span>
|
||||
<span>{totalCount} {t('hierarchy.actors')}</span>
|
||||
{selectedCount > 0 && (
|
||||
<span> ({selectedCount} {locale === 'zh' ? '个已选中' : 'selected'})</span>
|
||||
<span> ({selectedCount} {t('hierarchy.selected')})</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -884,6 +882,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
locale={locale}
|
||||
t={t}
|
||||
entityId={contextMenu.entityId}
|
||||
pluginTemplates={pluginTemplates}
|
||||
onCreateEmpty={() => { handleCreateEntity(); closeContextMenu(); }}
|
||||
@@ -904,6 +903,7 @@ interface ContextMenuWithSubmenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
locale: string;
|
||||
t: (key: string, params?: Record<string, string | number>, fallback?: string) => string;
|
||||
entityId: number | null;
|
||||
pluginTemplates: EntityCreationTemplate[];
|
||||
onCreateEmpty: () => void;
|
||||
@@ -914,43 +914,40 @@ interface ContextMenuWithSubmenuProps {
|
||||
}
|
||||
|
||||
function ContextMenuWithSubmenu({
|
||||
x, y, locale, entityId, pluginTemplates,
|
||||
x, y, locale, t, entityId, pluginTemplates,
|
||||
onCreateEmpty, onCreateFolder, onCreateFromTemplate, onDelete
|
||||
}: ContextMenuWithSubmenuProps) {
|
||||
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
|
||||
const [submenuPosition, setSubmenuPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const categoryLabels: Record<string, { zh: string; en: string }> = {
|
||||
'basic': { zh: '基础', en: 'Basic' },
|
||||
'rendering': { zh: '2D 对象', en: '2D Objects' },
|
||||
'ui': { zh: 'UI', en: 'UI' },
|
||||
'physics': { zh: '物理', en: 'Physics' },
|
||||
'audio': { zh: '音频', en: 'Audio' },
|
||||
'effects': { zh: '特效', en: 'Effects' },
|
||||
'other': { zh: '其他', en: 'Other' },
|
||||
};
|
||||
|
||||
// 实体创建模板的 label 本地化映射
|
||||
const entityTemplateLabels: Record<string, { zh: string; en: string }> = {
|
||||
'Sprite': { zh: '精灵', en: 'Sprite' },
|
||||
'Animated Sprite': { zh: '动画精灵', en: 'Animated Sprite' },
|
||||
'创建 Tilemap': { zh: '瓦片地图', en: 'Tilemap' },
|
||||
'Camera 2D': { zh: '2D 相机', en: 'Camera 2D' },
|
||||
'创建粒子效果': { zh: '粒子效果', en: 'Particle Effect' },
|
||||
};
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels = categoryLabels[category];
|
||||
return labels ? (locale === 'zh' ? labels.zh : labels.en) : category;
|
||||
// Map category keys to translation keys
|
||||
const categoryKeyMap: Record<string, string> = {
|
||||
'rendering': 'hierarchy.categories.rendering',
|
||||
'ui': 'hierarchy.categories.ui',
|
||||
'effects': 'hierarchy.categories.effects',
|
||||
'physics': 'hierarchy.categories.physics',
|
||||
'audio': 'hierarchy.categories.audio',
|
||||
'basic': 'hierarchy.categories.basic',
|
||||
'other': 'hierarchy.categories.other'
|
||||
};
|
||||
const key = categoryKeyMap[category];
|
||||
return key ? t(key) : category;
|
||||
};
|
||||
|
||||
const getEntityTemplateLabel = (label: string) => {
|
||||
const mapping = entityTemplateLabels[label];
|
||||
if (mapping) {
|
||||
return locale === 'zh' ? mapping.zh : mapping.en;
|
||||
}
|
||||
return label;
|
||||
// Map template labels to translation keys
|
||||
const templateKeyMap: Record<string, string> = {
|
||||
'Sprite': 'hierarchy.entityTemplates.sprite',
|
||||
'Animated Sprite': 'hierarchy.entityTemplates.animatedSprite',
|
||||
'创建 Tilemap': 'hierarchy.entityTemplates.tilemap',
|
||||
'Camera 2D': 'hierarchy.entityTemplates.camera2d',
|
||||
'创建粒子效果': 'hierarchy.entityTemplates.particleEffect'
|
||||
};
|
||||
const key = templateKeyMap[label];
|
||||
return key ? t(key) : label;
|
||||
};
|
||||
|
||||
const templatesByCategory = pluginTemplates.reduce((acc, template) => {
|
||||
@@ -985,12 +982,12 @@ function ContextMenuWithSubmenu({
|
||||
>
|
||||
<button onClick={onCreateEmpty}>
|
||||
<Plus size={12} />
|
||||
<span>{locale === 'zh' ? '创建空实体' : 'Create Empty Entity'}</span>
|
||||
<span>{t('hierarchy.createEmptyEntity')}</span>
|
||||
</button>
|
||||
|
||||
<button onClick={onCreateFolder}>
|
||||
<Folder size={12} />
|
||||
<span>{locale === 'zh' ? '创建文件夹' : 'Create Folder'}</span>
|
||||
<span>{t('hierarchy.createFolder')}</span>
|
||||
</button>
|
||||
|
||||
{sortedCategories.length > 0 && <div className="context-menu-divider" />}
|
||||
@@ -1029,7 +1026,7 @@ function ContextMenuWithSubmenu({
|
||||
<div className="context-menu-divider" />
|
||||
<button onClick={onDelete} className="context-menu-danger">
|
||||
<Trash2 size={12} />
|
||||
<span>{locale === 'zh' ? '删除实体' : 'Delete Entity'}</span>
|
||||
<span>{t('hierarchy.deleteEntity')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -13,9 +13,10 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager, ModuleManifest } from '@esengine/editor-core';
|
||||
import { SettingsRegistry, SettingCategory, SettingDescriptor, ProjectService, PluginManager, IPluginManager, ModuleManifest, isTranslationKey, getTranslationKey } from '@esengine/editor-core';
|
||||
import { PluginListSetting } from './PluginListSetting';
|
||||
import { ModuleListSetting } from './ModuleListSetting';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/SettingsWindow.css';
|
||||
|
||||
interface SettingsWindowProps {
|
||||
@@ -32,59 +33,87 @@ interface MainCategory {
|
||||
}
|
||||
|
||||
export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: SettingsWindowProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
/**
|
||||
* Resolve localizable text - if it starts with '$', treat as translation key
|
||||
* 解析可本地化文本 - 如果以 '$' 开头,作为翻译键处理
|
||||
*/
|
||||
const resolveText = (text: string | undefined): string => {
|
||||
if (!text) return '';
|
||||
if (isTranslationKey(text)) {
|
||||
return t(getTranslationKey(text));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const [categories, setCategories] = useState<SettingCategory[]>([]);
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(initialCategoryId || null);
|
||||
const [values, setValues] = useState<Map<string, any>>(new Map());
|
||||
const [errors, setErrors] = useState<Map<string, string>>(new Map());
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set());
|
||||
const [expandedMainCategories, setExpandedMainCategories] = useState<Set<string>>(new Set(['通用']));
|
||||
const [expandedMainCategories, setExpandedMainCategories] = useState<Set<string>>(new Set(['general']));
|
||||
|
||||
// 将分类组织成主分类和子分类
|
||||
// Organize categories into main categories and sub-categories
|
||||
const mainCategories = useMemo((): MainCategory[] => {
|
||||
const categoryMap = new Map<string, SettingCategory[]>();
|
||||
|
||||
// 定义主分类映射
|
||||
// 定义主分类映射(使用配置 ID 作为键)
|
||||
// Main category mapping (using config IDs as keys)
|
||||
const mainCategoryMapping: Record<string, string> = {
|
||||
'appearance': '通用',
|
||||
'general': '通用',
|
||||
'project': '通用',
|
||||
'plugins': '通用',
|
||||
'editor': '通用',
|
||||
'physics': '全局',
|
||||
'rendering': '全局',
|
||||
'audio': '全局',
|
||||
'world': '世界分区',
|
||||
'local': '世界分区(本地)',
|
||||
'performance': '性能'
|
||||
'appearance': 'general',
|
||||
'general': 'general',
|
||||
'project': 'general',
|
||||
'plugins': 'general',
|
||||
'editor': 'general',
|
||||
'physics': 'global',
|
||||
'rendering': 'global',
|
||||
'audio': 'global',
|
||||
'world': 'worldPartition',
|
||||
'local': 'worldPartitionLocal',
|
||||
'performance': 'performance'
|
||||
};
|
||||
|
||||
categories.forEach((cat) => {
|
||||
const mainCatName = mainCategoryMapping[cat.id] || '其他';
|
||||
if (!categoryMap.has(mainCatName)) {
|
||||
categoryMap.set(mainCatName, []);
|
||||
const mainCatId = mainCategoryMapping[cat.id] || 'other';
|
||||
if (!categoryMap.has(mainCatId)) {
|
||||
categoryMap.set(mainCatId, []);
|
||||
}
|
||||
categoryMap.get(mainCatName)!.push(cat);
|
||||
categoryMap.get(mainCatId)!.push(cat);
|
||||
});
|
||||
|
||||
// 定义固定的主分类顺序
|
||||
// 定义固定的主分类顺序(使用配置 ID)
|
||||
// Define fixed main category order (using config IDs)
|
||||
const orderedMainCategories = [
|
||||
'通用',
|
||||
'全局',
|
||||
'世界分区',
|
||||
'世界分区(本地)',
|
||||
'性能',
|
||||
'其他'
|
||||
'general',
|
||||
'global',
|
||||
'worldPartition',
|
||||
'worldPartitionLocal',
|
||||
'performance',
|
||||
'other'
|
||||
];
|
||||
|
||||
// 主分类 ID 到翻译键的映射
|
||||
// Main category ID to translation key mapping
|
||||
const categoryTranslationKeys: Record<string, string> = {
|
||||
'general': 'settingsWindow.mainCategories.general',
|
||||
'global': 'settingsWindow.mainCategories.global',
|
||||
'worldPartition': 'settingsWindow.mainCategories.worldPartition',
|
||||
'worldPartitionLocal': 'settingsWindow.mainCategories.worldPartitionLocal',
|
||||
'performance': 'settingsWindow.mainCategories.performance',
|
||||
'other': 'settingsWindow.mainCategories.other'
|
||||
};
|
||||
|
||||
return orderedMainCategories
|
||||
.filter((name) => categoryMap.has(name))
|
||||
.map((name) => ({
|
||||
id: name,
|
||||
title: name,
|
||||
subCategories: categoryMap.get(name)!
|
||||
.filter((id) => categoryMap.has(id))
|
||||
.map((id) => ({
|
||||
id,
|
||||
title: t(categoryTranslationKeys[id] || 'settingsWindow.mainCategories.other'),
|
||||
subCategories: categoryMap.get(id)!
|
||||
}));
|
||||
}, [categories]);
|
||||
}, [categories, t]);
|
||||
|
||||
// 获取显示的子分类标题
|
||||
const subCategoryTitle = useMemo(() => {
|
||||
@@ -170,9 +199,9 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
|
||||
const newErrors = new Map(errors);
|
||||
if (!settingsRegistry.validateSetting(descriptor, value)) {
|
||||
newErrors.set(key, descriptor.validator?.errorMessage || '无效值');
|
||||
newErrors.set(key, descriptor.validator?.errorMessage || t('settingsWindow.invalidValue'));
|
||||
setErrors(newErrors);
|
||||
return; // 验证失败,不保存
|
||||
return; // 验证失败,不保存 | Validation failed, don't save
|
||||
} else {
|
||||
newErrors.delete(key);
|
||||
}
|
||||
@@ -334,7 +363,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -354,7 +383,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -362,7 +391,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
className={`settings-number-input ${error ? 'error' : ''}`}
|
||||
value={value}
|
||||
onChange={(e) => handleValueChange(setting.key, parseInt(e.target.value) || 0, setting)}
|
||||
placeholder={setting.placeholder}
|
||||
placeholder={resolveText(setting.placeholder)}
|
||||
min={setting.min}
|
||||
max={setting.max}
|
||||
step={setting.step}
|
||||
@@ -378,7 +407,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -386,7 +415,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
className={`settings-text-input ${error ? 'error' : ''}`}
|
||||
value={value}
|
||||
onChange={(e) => handleValueChange(setting.key, e.target.value, setting)}
|
||||
placeholder={setting.placeholder}
|
||||
placeholder={resolveText(setting.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,7 +428,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<select
|
||||
@@ -414,7 +443,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
>
|
||||
{setting.options?.map((option) => (
|
||||
<option key={String(option.value)} value={String(option.value)}>
|
||||
{option.label}
|
||||
{resolveText(option.label)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -429,7 +458,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<input
|
||||
@@ -453,7 +482,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{setting.description && (
|
||||
<ChevronRight size={12} className="settings-row-expand" />
|
||||
)}
|
||||
<span>{setting.label}</span>
|
||||
<span>{resolveText(setting.label)}</span>
|
||||
</div>
|
||||
<div className="settings-row-value">
|
||||
<div className="settings-color-bar" style={{ backgroundColor: value }}>
|
||||
@@ -473,7 +502,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
if (!pluginManager) {
|
||||
return (
|
||||
<div className="settings-row">
|
||||
<p className="settings-error">PluginManager 不可用</p>
|
||||
<p className="settings-error">{t('settingsWindow.pluginManagerUnavailable')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -495,7 +524,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
}
|
||||
return (
|
||||
<div className="settings-row">
|
||||
<p className="settings-hint">碰撞矩阵编辑器未配置</p>
|
||||
<p className="settings-hint">{t('settingsWindow.collisionMatrixNotConfigured')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -539,14 +568,14 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
<div className="settings-sidebar-new">
|
||||
<div className="settings-sidebar-header">
|
||||
<SettingsIcon size={16} />
|
||||
<span>编辑器偏好设置</span>
|
||||
<span>{t('settingsWindow.editorPreferences')}</span>
|
||||
<button className="settings-sidebar-close" onClick={handleCancel}>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="settings-sidebar-search">
|
||||
<span>所有设置</span>
|
||||
<span>{t('settingsWindow.allSettings')}</span>
|
||||
</div>
|
||||
|
||||
<div className="settings-sidebar-categories">
|
||||
@@ -572,7 +601,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
className={`settings-sub-category ${selectedCategoryId === subCat.id ? 'active' : ''}`}
|
||||
onClick={() => setSelectedCategoryId(subCat.id)}
|
||||
>
|
||||
{subCat.title}
|
||||
{resolveText(subCat.title)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -590,20 +619,20 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索"
|
||||
placeholder={t('settingsWindow.search')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="settings-header-actions">
|
||||
<button className="settings-icon-btn" title="设置">
|
||||
<button className="settings-icon-btn" title={t('settingsWindow.settingsBtn')}>
|
||||
<SettingsIcon size={14} />
|
||||
</button>
|
||||
<button className="settings-action-btn" onClick={handleExport}>
|
||||
导出......
|
||||
{t('settingsWindow.export')}
|
||||
</button>
|
||||
<button className="settings-action-btn" onClick={handleImport}>
|
||||
导入......
|
||||
{t('settingsWindow.import')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -617,20 +646,17 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
<span className="settings-breadcrumb-sub">{subCategoryTitle}</span>
|
||||
</div>
|
||||
{selectedCategory?.description && (
|
||||
<p className="settings-category-desc">{selectedCategory.description}</p>
|
||||
<p className="settings-category-desc">{resolveText(selectedCategory.description)}</p>
|
||||
)}
|
||||
<div className="settings-category-actions">
|
||||
<button className="settings-category-action-btn" onClick={handleResetToDefault}>
|
||||
设置为默认值
|
||||
{t('settingsWindow.resetToDefault')}
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleExport}>
|
||||
导出......
|
||||
{t('settingsWindow.export')}
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleImport}>
|
||||
导入......
|
||||
</button>
|
||||
<button className="settings-category-action-btn" onClick={handleResetToDefault}>
|
||||
重置为默认
|
||||
{t('settingsWindow.import')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -652,7 +678,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
) : (
|
||||
<ChevronRight size={12} />
|
||||
)}
|
||||
<span>{section.title}</span>
|
||||
<span>{resolveText(section.title)}</span>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
@@ -671,7 +697,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
|
||||
{!selectedCategory && (
|
||||
<div className="settings-empty-new">
|
||||
<SettingsIcon size={48} />
|
||||
<p>请选择一个设置分类</p>
|
||||
<p>{t('settingsWindow.selectCategory')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,9 @@ import { Globe, ChevronDown, Download, X, Loader2, Trash2, CheckCircle, AlertCir
|
||||
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
||||
import { StartupLogo } from './StartupLogo';
|
||||
import { TauriAPI, type EnvironmentCheckResult } from '../api/tauri';
|
||||
import { useLocale, type Locale } from '../hooks/useLocale';
|
||||
import '../styles/StartupPage.css';
|
||||
|
||||
type Locale = 'en' | 'zh';
|
||||
|
||||
interface StartupPageProps {
|
||||
onOpenProject: () => void;
|
||||
onCreateProject: () => void;
|
||||
@@ -16,7 +15,6 @@ interface StartupPageProps {
|
||||
onDeleteProject?: (projectPath: string) => Promise<void>;
|
||||
onLocaleChange?: (locale: Locale) => void;
|
||||
recentProjects?: string[];
|
||||
locale: string;
|
||||
}
|
||||
|
||||
const LANGUAGES = [
|
||||
@@ -24,7 +22,8 @@ const LANGUAGES = [
|
||||
{ code: 'zh', name: '中文' }
|
||||
];
|
||||
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [] }: StartupPageProps) {
|
||||
const { t, locale } = useLocale();
|
||||
const [showLogo, setShowLogo] = useState(true);
|
||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
@@ -80,53 +79,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
});
|
||||
}, []);
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'ESEngine Editor',
|
||||
subtitle: 'Professional Game Development Tool',
|
||||
openProject: 'Open Project',
|
||||
createProject: 'Create Project',
|
||||
recentProjects: 'Recent Projects',
|
||||
noRecentProjects: 'No recent projects',
|
||||
updateAvailable: 'New version available',
|
||||
updateNow: 'Update Now',
|
||||
installing: 'Installing...',
|
||||
later: 'Later',
|
||||
removeFromList: 'Remove from List',
|
||||
deleteProject: 'Delete Project',
|
||||
deleteConfirmTitle: 'Delete Project',
|
||||
deleteConfirmMessage: 'Are you sure you want to permanently delete this project? This action cannot be undone.',
|
||||
cancel: 'Cancel',
|
||||
delete: 'Delete',
|
||||
envReady: 'Environment Ready',
|
||||
envNotReady: 'Environment Issue',
|
||||
esbuildReady: 'esbuild ready',
|
||||
esbuildMissing: 'esbuild not found'
|
||||
},
|
||||
zh: {
|
||||
title: 'ESEngine 编辑器',
|
||||
subtitle: '专业游戏开发工具',
|
||||
openProject: '打开项目',
|
||||
createProject: '创建新项目',
|
||||
recentProjects: '最近的项目',
|
||||
noRecentProjects: '没有最近的项目',
|
||||
updateAvailable: '发现新版本',
|
||||
updateNow: '立即更新',
|
||||
installing: '正在安装...',
|
||||
later: '稍后',
|
||||
removeFromList: '从列表中移除',
|
||||
deleteProject: '删除项目',
|
||||
deleteConfirmTitle: '删除项目',
|
||||
deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
envReady: '环境就绪',
|
||||
envNotReady: '环境问题',
|
||||
esbuildReady: 'esbuild 就绪',
|
||||
esbuildMissing: '未找到 esbuild'
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstallUpdate = async () => {
|
||||
setIsInstalling(true);
|
||||
const success = await installUpdate();
|
||||
@@ -136,8 +88,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
// 如果成功,应用会重启,不需要处理
|
||||
};
|
||||
|
||||
const t = translations[locale as keyof typeof translations] || translations.en;
|
||||
const versionText = locale === 'zh' ? `版本 ${appVersion}` : `Version ${appVersion}`;
|
||||
const versionText = `${t('startup.version')} ${appVersion}`;
|
||||
|
||||
const handleLogoComplete = () => {
|
||||
setShowLogo(false);
|
||||
@@ -147,8 +98,8 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="startup-page">
|
||||
{showLogo && <StartupLogo onAnimationComplete={handleLogoComplete} />}
|
||||
<div className="startup-header">
|
||||
<h1 className="startup-title">{t.title}</h1>
|
||||
<p className="startup-subtitle">{t.subtitle}</p>
|
||||
<h1 className="startup-title">{t('startup.title')}</h1>
|
||||
<p className="startup-subtitle">{t('startup.subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<div className="startup-content">
|
||||
@@ -157,21 +108,21 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
||||
</svg>
|
||||
<span>{t.openProject}</span>
|
||||
<span>{t('startup.openProject')}</span>
|
||||
</button>
|
||||
|
||||
<button className="startup-action-btn" onClick={onCreateProject}>
|
||||
<svg className="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path d="M12 5V19M5 12H19" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
<span>{t.createProject}</span>
|
||||
<span>{t('startup.createProject')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="startup-recent">
|
||||
<h2 className="recent-title">{t.recentProjects}</h2>
|
||||
<h2 className="recent-title">{t('startup.recentProjects')}</h2>
|
||||
{recentProjects.length === 0 ? (
|
||||
<p className="recent-empty">{t.noRecentProjects}</p>
|
||||
<p className="recent-empty">{t('startup.noRecentProjects')}</p>
|
||||
) : (
|
||||
<ul className="recent-list">
|
||||
{recentProjects.map((project, index) => (
|
||||
@@ -201,7 +152,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
e.stopPropagation();
|
||||
onRemoveRecentProject(project);
|
||||
}}
|
||||
title={t.removeFromList}
|
||||
title={t('startup.removeFromList')}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
@@ -219,7 +170,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="update-banner-content">
|
||||
<Download size={16} />
|
||||
<span className="update-banner-text">
|
||||
{t.updateAvailable}: v{updateInfo.version}
|
||||
{t('startup.updateAvailable')}: v{updateInfo.version}
|
||||
</span>
|
||||
<button
|
||||
className="update-banner-btn primary"
|
||||
@@ -229,17 +180,17 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
{t.installing}
|
||||
{t('startup.installing')}
|
||||
</>
|
||||
) : (
|
||||
t.updateNow
|
||||
t('startup.updateNow')
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="update-banner-close"
|
||||
onClick={() => setShowUpdateBanner(false)}
|
||||
disabled={isInstalling}
|
||||
title={t.later}
|
||||
title={t('startup.later')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -255,7 +206,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div
|
||||
className={`startup-env-status ${envCheck.ready ? 'ready' : 'warning'}`}
|
||||
onClick={() => setShowEnvStatus(!showEnvStatus)}
|
||||
title={envCheck.ready ? t.envReady : t.envNotReady}
|
||||
title={envCheck.ready ? t('startup.envReady') : t('startup.envNotReady')}
|
||||
>
|
||||
{envCheck.ready ? (
|
||||
<CheckCircle size={14} />
|
||||
@@ -265,7 +216,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
{showEnvStatus && (
|
||||
<div className="startup-env-tooltip">
|
||||
<div className="env-tooltip-title">
|
||||
{envCheck.ready ? t.envReady : t.envNotReady}
|
||||
{envCheck.ready ? t('startup.envReady') : t('startup.envNotReady')}
|
||||
</div>
|
||||
<div className={`env-tooltip-item ${envCheck.esbuild.available ? 'ok' : 'error'}`}>
|
||||
{envCheck.esbuild.available ? (
|
||||
@@ -277,7 +228,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle size={12} />
|
||||
<span>{t.esbuildMissing}</span>
|
||||
<span>{t('startup.esbuildMissing')}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -335,7 +286,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
<span>{t.removeFromList}</span>
|
||||
<span>{t('startup.removeFromList')}</span>
|
||||
</button>
|
||||
{onDeleteProject && (
|
||||
<button
|
||||
@@ -346,7 +297,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
}}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>{t.deleteProject}</span>
|
||||
<span>{t('startup.deleteProject')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -359,10 +310,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="startup-dialog">
|
||||
<div className="startup-dialog-header">
|
||||
<Trash2 size={20} className="dialog-icon-danger" />
|
||||
<h3>{t.deleteConfirmTitle}</h3>
|
||||
<h3>{t('startup.deleteConfirmTitle')}</h3>
|
||||
</div>
|
||||
<div className="startup-dialog-body">
|
||||
<p>{t.deleteConfirmMessage}</p>
|
||||
<p>{t('startup.deleteConfirmMessage')}</p>
|
||||
<p className="startup-dialog-path">{deleteConfirm}</p>
|
||||
</div>
|
||||
<div className="startup-dialog-footer">
|
||||
@@ -370,7 +321,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
className="startup-dialog-btn"
|
||||
onClick={() => setDeleteConfirm(null)}
|
||||
>
|
||||
{t.cancel}
|
||||
{t('startup.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="startup-dialog-btn danger"
|
||||
@@ -386,7 +337,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
>
|
||||
{t.delete}
|
||||
{t('startup.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FolderOpen, FileText, Terminal, ChevronDown, ChevronUp, Activity, Wifi,
|
||||
import type { MessageHub, LogService } from '@esengine/editor-core';
|
||||
import { ContentBrowser } from './ContentBrowser';
|
||||
import { OutputLogPanel } from './OutputLogPanel';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/StatusBar.css';
|
||||
|
||||
interface StatusBarProps {
|
||||
@@ -26,6 +27,7 @@ export function StatusBar({
|
||||
projectPath,
|
||||
onOpenScene
|
||||
}: StatusBarProps) {
|
||||
const { t } = useLocale();
|
||||
const [consoleInput, setConsoleInput] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>('output');
|
||||
const [contentDrawerOpen, setContentDrawerOpen] = useState(false);
|
||||
@@ -254,7 +256,7 @@ export function StatusBar({
|
||||
onClick={handleContentDrawerClick}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
<span>{locale === 'zh' ? '内容侧滑菜单' : 'Content Drawer'}</span>
|
||||
<span>{t('statusBar.contentDrawer')}</span>
|
||||
{contentDrawerOpen ? <ChevronDown size={12} /> : <ChevronUp size={12} />}
|
||||
</button>
|
||||
|
||||
@@ -265,7 +267,7 @@ export function StatusBar({
|
||||
onClick={handleOutputLogClick}
|
||||
>
|
||||
<FileText size={12} />
|
||||
<span>{locale === 'zh' ? '输出日志' : 'Output Log'}</span>
|
||||
<span>{t('statusBar.outputLog')}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -282,7 +284,7 @@ export function StatusBar({
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder={locale === 'zh' ? '输入控制台命令' : 'Enter Console Command'}
|
||||
placeholder={t('statusBar.consolePlaceholder')}
|
||||
value={consoleInput}
|
||||
onChange={(e) => setConsoleInput(e.target.value)}
|
||||
onKeyDown={handleConsoleSubmit}
|
||||
@@ -294,17 +296,17 @@ export function StatusBar({
|
||||
<div className="status-bar-right">
|
||||
<button className="status-bar-indicator">
|
||||
<Activity size={12} />
|
||||
<span>{locale === 'zh' ? '回追踪' : 'Trace'}</span>
|
||||
<span>{t('statusBar.trace')}</span>
|
||||
<ChevronDown size={10} />
|
||||
</button>
|
||||
|
||||
<div className="status-bar-divider" />
|
||||
|
||||
<div className="status-bar-icon-group">
|
||||
<button className="status-bar-icon-btn" title={locale === 'zh' ? '网络' : 'Network'}>
|
||||
<button className="status-bar-icon-btn" title={t('statusBar.network')}>
|
||||
<Wifi size={14} />
|
||||
</button>
|
||||
<button className="status-bar-icon-btn" title={locale === 'zh' ? '源代码管理' : 'Source Control'}>
|
||||
<button className="status-bar-icon-btn" title={t('statusBar.sourceControl')}>
|
||||
<GitBranch size={14} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -313,11 +315,11 @@ export function StatusBar({
|
||||
|
||||
<div className="status-bar-info">
|
||||
<Save size={12} />
|
||||
<span>{locale === 'zh' ? '所有已保存' : 'All Saved'}</span>
|
||||
<span>{t('statusBar.allSaved')}</span>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-info">
|
||||
<span>{locale === 'zh' ? '版本控制' : 'Revision Control'}</span>
|
||||
<span>{t('statusBar.revisionControl')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { UIRegistry, MessageHub, PluginManager } from '@esengine/editor-core';
|
||||
import type { MenuItem as PluginMenuItem } from '@esengine/editor-core';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import '../styles/TitleBar.css';
|
||||
|
||||
interface MenuItem {
|
||||
@@ -17,7 +18,6 @@ interface MenuItem {
|
||||
|
||||
interface TitleBarProps {
|
||||
projectName?: string;
|
||||
locale?: string;
|
||||
uiRegistry?: UIRegistry;
|
||||
messageHub?: MessageHub;
|
||||
pluginManager?: PluginManager;
|
||||
@@ -41,7 +41,6 @@ interface TitleBarProps {
|
||||
|
||||
export function TitleBar({
|
||||
projectName = 'Untitled',
|
||||
locale = 'en',
|
||||
uiRegistry,
|
||||
messageHub,
|
||||
pluginManager,
|
||||
@@ -62,6 +61,7 @@ export function TitleBar({
|
||||
onReloadPlugins,
|
||||
onOpenBuildSettings
|
||||
}: TitleBarProps) {
|
||||
const { t } = useLocale();
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||
const [pluginMenuItems, setPluginMenuItems] = useState<PluginMenuItem[]>([]);
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
@@ -119,109 +119,31 @@ export function TitleBar({
|
||||
};
|
||||
}, []);
|
||||
|
||||
const t = (key: string) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
en: {
|
||||
file: 'File',
|
||||
newScene: 'New Scene',
|
||||
openScene: 'Open Scene',
|
||||
saveScene: 'Save Scene',
|
||||
saveSceneAs: 'Save Scene As...',
|
||||
openProject: 'Open Project',
|
||||
closeProject: 'Close Project',
|
||||
exit: 'Exit',
|
||||
edit: 'Edit',
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
cut: 'Cut',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste',
|
||||
delete: 'Delete',
|
||||
selectAll: 'Select All',
|
||||
window: 'Window',
|
||||
sceneHierarchy: 'Scene Hierarchy',
|
||||
inspector: 'Inspector',
|
||||
assets: 'Assets',
|
||||
console: 'Console',
|
||||
viewport: 'Viewport',
|
||||
pluginManager: 'Plugin Manager',
|
||||
tools: 'Tools',
|
||||
createPlugin: 'Create Plugin',
|
||||
reloadPlugins: 'Reload Plugins',
|
||||
portManager: 'Port Manager',
|
||||
settings: 'Settings',
|
||||
help: 'Help',
|
||||
documentation: 'Documentation',
|
||||
checkForUpdates: 'Check for Updates',
|
||||
about: 'About',
|
||||
devtools: 'Developer Tools',
|
||||
buildSettings: 'Build Settings'
|
||||
},
|
||||
zh: {
|
||||
file: '文件',
|
||||
newScene: '新建场景',
|
||||
openScene: '打开场景',
|
||||
saveScene: '保存场景',
|
||||
saveSceneAs: '场景另存为...',
|
||||
openProject: '打开项目',
|
||||
closeProject: '关闭项目',
|
||||
exit: '退出',
|
||||
edit: '编辑',
|
||||
undo: '撤销',
|
||||
redo: '重做',
|
||||
cut: '剪切',
|
||||
copy: '复制',
|
||||
paste: '粘贴',
|
||||
delete: '删除',
|
||||
selectAll: '全选',
|
||||
window: '窗口',
|
||||
sceneHierarchy: '场景层级',
|
||||
inspector: '检视器',
|
||||
assets: '资产',
|
||||
console: '控制台',
|
||||
viewport: '视口',
|
||||
pluginManager: '插件管理器',
|
||||
tools: '工具',
|
||||
createPlugin: '创建插件',
|
||||
reloadPlugins: '重新加载插件',
|
||||
portManager: '端口管理器',
|
||||
settings: '设置',
|
||||
help: '帮助',
|
||||
documentation: '文档',
|
||||
checkForUpdates: '检查更新',
|
||||
about: '关于',
|
||||
devtools: '开发者工具',
|
||||
buildSettings: '构建设置'
|
||||
}
|
||||
};
|
||||
return translations[locale]?.[key] || key;
|
||||
};
|
||||
|
||||
const menus: Record<string, MenuItem[]> = {
|
||||
file: [
|
||||
{ label: t('newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ label: t('menu.file.newScene'), shortcut: 'Ctrl+N', onClick: onNewScene },
|
||||
{ label: t('menu.file.openScene'), shortcut: 'Ctrl+O', onClick: onOpenScene },
|
||||
{ separator: true },
|
||||
{ label: t('saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ label: t('menu.file.saveScene'), shortcut: 'Ctrl+S', onClick: onSaveScene },
|
||||
{ label: t('menu.file.saveSceneAs'), shortcut: 'Ctrl+Shift+S', onClick: onSaveSceneAs },
|
||||
{ separator: true },
|
||||
{ label: t('buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ label: t('menu.file.buildSettings'), shortcut: 'Ctrl+Shift+B', onClick: onOpenBuildSettings },
|
||||
{ separator: true },
|
||||
{ label: t('openProject'), onClick: onOpenProject },
|
||||
{ label: t('closeProject'), onClick: onCloseProject },
|
||||
{ label: t('menu.file.openProject'), onClick: onOpenProject },
|
||||
{ label: t('menu.file.closeProject'), onClick: onCloseProject },
|
||||
{ separator: true },
|
||||
{ label: t('exit'), onClick: onExit }
|
||||
{ label: t('menu.file.exit'), onClick: onExit }
|
||||
],
|
||||
edit: [
|
||||
{ label: t('undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ label: t('menu.edit.undo'), shortcut: 'Ctrl+Z', disabled: true },
|
||||
{ label: t('menu.edit.redo'), shortcut: 'Ctrl+Y', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('delete'), shortcut: 'Delete', disabled: true },
|
||||
{ label: t('menu.edit.cut'), shortcut: 'Ctrl+X', disabled: true },
|
||||
{ label: t('menu.edit.copy'), shortcut: 'Ctrl+C', disabled: true },
|
||||
{ label: t('menu.edit.paste'), shortcut: 'Ctrl+V', disabled: true },
|
||||
{ label: t('menu.edit.delete'), shortcut: 'Delete', disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
{ label: t('menu.edit.selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
],
|
||||
window: [
|
||||
...pluginMenuItems.map((item) => ({
|
||||
@@ -231,25 +153,34 @@ export function TitleBar({
|
||||
onClick: item.onClick
|
||||
})),
|
||||
...(pluginMenuItems.length > 0 ? [{ separator: true } as MenuItem] : []),
|
||||
{ label: t('pluginManager'), onClick: onOpenPluginManager },
|
||||
{ label: t('menu.window.pluginManager'), onClick: onOpenPluginManager },
|
||||
{ separator: true },
|
||||
{ label: t('devtools'), onClick: onToggleDevtools }
|
||||
{ label: t('menu.window.devtools'), onClick: onToggleDevtools }
|
||||
],
|
||||
tools: [
|
||||
{ label: t('createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ label: t('menu.tools.createPlugin'), onClick: onCreatePlugin },
|
||||
{ label: t('menu.tools.reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
|
||||
{ separator: true },
|
||||
{ label: t('portManager'), onClick: onOpenPortManager },
|
||||
{ label: t('menu.tools.portManager'), onClick: onOpenPortManager },
|
||||
{ separator: true },
|
||||
{ label: t('settings'), onClick: onOpenSettings }
|
||||
{ label: t('menu.tools.settings'), onClick: onOpenSettings }
|
||||
],
|
||||
help: [
|
||||
{ label: t('documentation'), disabled: true },
|
||||
{ label: t('menu.help.documentation'), disabled: true },
|
||||
{ separator: true },
|
||||
{ label: t('about'), onClick: onOpenAbout }
|
||||
{ label: t('menu.help.about'), onClick: onOpenAbout }
|
||||
]
|
||||
};
|
||||
|
||||
// 菜单键到翻译键的映射 | Map menu keys to translation keys
|
||||
const menuTitleKeys: Record<string, string> = {
|
||||
file: 'menu.file.title',
|
||||
edit: 'menu.edit.title',
|
||||
window: 'menu.window.title',
|
||||
tools: 'menu.tools.title',
|
||||
help: 'menu.help.title'
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||
@@ -300,7 +231,7 @@ export function TitleBar({
|
||||
className={`titlebar-menu-button ${openMenu === menuKey ? 'active' : ''}`}
|
||||
onClick={() => handleMenuClick(menuKey)}
|
||||
>
|
||||
{t(menuKey)}
|
||||
{t(menuTitleKeys[menuKey] || menuKey)}
|
||||
</button>
|
||||
{openMenu === menuKey && menus[menuKey] && (
|
||||
<div className="titlebar-dropdown">
|
||||
@@ -338,12 +269,12 @@ export function TitleBar({
|
||||
<div className="titlebar-right">
|
||||
<span className="titlebar-project-name" data-tauri-drag-region>{projectName}</span>
|
||||
<div className="titlebar-window-controls">
|
||||
<button className="titlebar-button" onClick={handleMinimize} title="Minimize">
|
||||
<button className="titlebar-button" onClick={handleMinimize} title={t('titleBar.minimize')}>
|
||||
<svg width="10" height="1" viewBox="0 0 10 1">
|
||||
<rect width="10" height="1" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button className="titlebar-button" onClick={handleMaximize} title={isMaximized ? "Restore" : "Maximize"}>
|
||||
<button className="titlebar-button" onClick={handleMaximize} title={isMaximized ? t('titleBar.restore') : t('titleBar.maximize')}>
|
||||
{isMaximized ? (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M2 0v2H0v8h8V8h2V0H2zm6 8H2V4h6v4z" fill="currentColor"/>
|
||||
@@ -354,7 +285,7 @@ export function TitleBar({
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
<button className="titlebar-button titlebar-button-close" onClick={handleClose} title="Close">
|
||||
<button className="titlebar-button titlebar-button-close" onClick={handleClose} title={t('titleBar.close')}>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/>
|
||||
</svg>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import '../styles/Viewport.css';
|
||||
import { useEngine } from '../hooks/useEngine';
|
||||
import { useLocale } from '../hooks/useLocale';
|
||||
import { EngineService } from '../services/EngineService';
|
||||
import { Core, Entity, SceneSerializer } from '@esengine/ecs-framework';
|
||||
import { MessageHub, ProjectService, AssetRegistryService } from '@esengine/editor-core';
|
||||
@@ -207,6 +208,7 @@ interface ViewportProps {
|
||||
}
|
||||
|
||||
export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
const { t } = useLocale();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [playState, setPlayState] = useState<PlayState>('stopped');
|
||||
@@ -704,9 +706,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
// Check if there's a camera entity
|
||||
const cameraEntity = findPlayerCamera();
|
||||
if (!cameraEntity) {
|
||||
const warningMessage = locale === 'zh'
|
||||
? '缺少相机: 场景中没有相机实体,请添加一个带有Camera组件的实体'
|
||||
: 'Missing Camera: No camera entity in scene. Please add an entity with Camera component.';
|
||||
const warningMessage = t('viewport.errors.missingCamera');
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:show', {
|
||||
message: warningMessage,
|
||||
@@ -781,8 +781,8 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
const scene = engineService.getScene();
|
||||
if (!scene) {
|
||||
messageHub?.publish('notification:error', {
|
||||
title: locale === 'zh' ? '错误' : 'Error',
|
||||
message: locale === 'zh' ? '没有可运行的场景' : 'No scene to run'
|
||||
title: t('common.error'),
|
||||
message: t('viewport.errors.noScene')
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1008,13 +1008,13 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
await open(serverUrl);
|
||||
|
||||
messageHub?.publish('notification:success', {
|
||||
title: locale === 'zh' ? '浏览器运行' : 'Run in Browser',
|
||||
message: locale === 'zh' ? `已在浏览器中打开: ${serverUrl}` : `Opened in browser: ${serverUrl}`
|
||||
title: t('viewport.run.inBrowser'),
|
||||
message: t('viewport.run.openedInBrowser', { url: serverUrl })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to run in browser:', error);
|
||||
messageHub?.publish('notification:error', {
|
||||
title: locale === 'zh' ? '运行失败' : 'Run Failed',
|
||||
title: t('viewport.run.failed'),
|
||||
message: String(error)
|
||||
});
|
||||
}
|
||||
@@ -1026,8 +1026,8 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
if (!Core.scene) {
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:warning', {
|
||||
title: locale === 'zh' ? '无场景' : 'No Scene',
|
||||
message: locale === 'zh' ? '请先创建场景' : 'Please create a scene first'
|
||||
title: t('viewport.notifications.noScene'),
|
||||
message: t('viewport.errors.noSceneFirst')
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -1140,15 +1140,15 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:success', {
|
||||
title: locale === 'zh' ? '服务器已启动' : 'Server Started',
|
||||
message: locale === 'zh' ? `预览地址: ${previewUrl}` : `Preview URL: ${previewUrl}`
|
||||
title: t('viewport.run.serverStarted'),
|
||||
message: t('viewport.run.previewUrl', { url: previewUrl })
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to run on device:', error);
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:error', {
|
||||
title: locale === 'zh' ? '启动失败' : 'Failed to Start',
|
||||
title: t('viewport.run.startFailed'),
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
@@ -1281,28 +1281,28 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'select' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('select')}
|
||||
title={locale === 'zh' ? '选择 (Q)' : 'Select (Q)'}
|
||||
title={t('viewport.tools.select')}
|
||||
>
|
||||
<MousePointer2 size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'move' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('move')}
|
||||
title={locale === 'zh' ? '移动 (W)' : 'Move (W)'}
|
||||
title={t('viewport.tools.move')}
|
||||
>
|
||||
<Move size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'rotate' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('rotate')}
|
||||
title={locale === 'zh' ? '旋转 (E)' : 'Rotate (E)'}
|
||||
title={t('viewport.tools.rotate')}
|
||||
>
|
||||
<RotateCw size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${transformMode === 'scale' ? 'active' : ''}`}
|
||||
onClick={() => setTransformMode('scale')}
|
||||
title={locale === 'zh' ? '缩放 (R)' : 'Scale (R)'}
|
||||
title={t('viewport.tools.scale')}
|
||||
>
|
||||
<Scaling size={14} />
|
||||
</button>
|
||||
@@ -1314,7 +1314,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${snapEnabled ? 'active' : ''}`}
|
||||
onClick={() => setSnapEnabled(!snapEnabled)}
|
||||
title={locale === 'zh' ? '吸附开关' : 'Toggle Snap'}
|
||||
title={t('viewport.snap.toggle')}
|
||||
>
|
||||
<Magnet size={14} />
|
||||
</button>
|
||||
@@ -1324,7 +1324,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowGridSnapMenu(!showGridSnapMenu); }}
|
||||
title={locale === 'zh' ? '网格吸附' : 'Grid Snap'}
|
||||
title={t('viewport.snap.grid')}
|
||||
>
|
||||
<Grid3x3 size={12} />
|
||||
<span>{gridSnapValue}</span>
|
||||
@@ -1350,7 +1350,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowRotationSnapMenu(!showRotationSnapMenu); }}
|
||||
title={locale === 'zh' ? '旋转吸附' : 'Rotation Snap'}
|
||||
title={t('viewport.snap.rotation')}
|
||||
>
|
||||
<RotateCw size={12} />
|
||||
<span>{rotationSnapValue}°</span>
|
||||
@@ -1376,7 +1376,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowScaleSnapMenu(!showScaleSnapMenu); }}
|
||||
title={locale === 'zh' ? '缩放吸附' : 'Scale Snap'}
|
||||
title={t('viewport.snap.scale')}
|
||||
>
|
||||
<Scaling size={12} />
|
||||
<span>{scaleSnapValue}</span>
|
||||
@@ -1403,14 +1403,14 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${showGrid ? 'active' : ''}`}
|
||||
onClick={() => setShowGrid(!showGrid)}
|
||||
title={locale === 'zh' ? '显示网格' : 'Show Grid'}
|
||||
title={t('viewport.view.showGrid')}
|
||||
>
|
||||
<Grid3x3 size={14} />
|
||||
</button>
|
||||
<button
|
||||
className={`viewport-btn ${showGizmos ? 'active' : ''}`}
|
||||
onClick={() => setShowGizmos(!showGizmos)}
|
||||
title={locale === 'zh' ? '显示辅助线' : 'Show Gizmos'}
|
||||
title={t('viewport.view.showGizmos')}
|
||||
>
|
||||
{showGizmos ? <Eye size={14} /> : <EyeOff size={14} />}
|
||||
</button>
|
||||
@@ -1429,7 +1429,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className={`viewport-btn ${showStats ? 'active' : ''}`}
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
title={locale === 'zh' ? '统计信息' : 'Stats'}
|
||||
title={t('viewport.view.stats')}
|
||||
>
|
||||
<Activity size={14} />
|
||||
</button>
|
||||
@@ -1438,7 +1438,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-btn"
|
||||
onClick={handleReset}
|
||||
title={locale === 'zh' ? '重置视图' : 'Reset View'}
|
||||
title={t('viewport.view.resetView')}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
</button>
|
||||
@@ -1447,7 +1447,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-btn"
|
||||
onClick={handleFullscreen}
|
||||
title={locale === 'zh' ? '全屏' : 'Fullscreen'}
|
||||
title={t('viewport.view.fullscreen')}
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
@@ -1459,7 +1459,7 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<button
|
||||
className="viewport-snap-btn"
|
||||
onClick={() => { closeAllSnapMenus(); setShowRunMenu(!showRunMenu); }}
|
||||
title={locale === 'zh' ? '运行选项' : 'Run Options'}
|
||||
title={t('viewport.run.options')}
|
||||
>
|
||||
<Globe size={14} />
|
||||
<ChevronDown size={10} />
|
||||
@@ -1468,11 +1468,11 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
<div className="viewport-snap-menu viewport-snap-menu-right">
|
||||
<button onClick={handleRunInBrowser}>
|
||||
<Globe size={14} />
|
||||
{locale === 'zh' ? '浏览器运行' : 'Run in Browser'}
|
||||
{t('viewport.run.inBrowser')}
|
||||
</button>
|
||||
<button onClick={handleRunOnDevice}>
|
||||
<QrCode size={14} />
|
||||
{locale === 'zh' ? '真机运行' : 'Run on Device'}
|
||||
{t('viewport.run.onDevice')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { LocaleService, type Locale } from '@esengine/editor-core';
|
||||
import { LocaleService, type TranslationParams, type LocaleInfo, type Locale } from '@esengine/editor-core';
|
||||
|
||||
// Re-export Locale type for convenience | 重新导出 Locale 类型以便使用
|
||||
export type { Locale } from '@esengine/editor-core';
|
||||
|
||||
/**
|
||||
* React Hook for internationalization
|
||||
* React 国际化 Hook
|
||||
*
|
||||
* 提供翻译函数、语言切换和语言变化监听。
|
||||
* Provides translation function, locale switching and locale change listening.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { t, locale, changeLocale, supportedLocales } = useLocale();
|
||||
*
|
||||
* return (
|
||||
* <div>
|
||||
* <h1>{t('app.title')}</h1>
|
||||
* <p>{t('scene.savedSuccess', { name: 'MyScene' })}</p>
|
||||
* <select value={locale} onChange={(e) => changeLocale(e.target.value as Locale)}>
|
||||
* {supportedLocales.map((loc) => (
|
||||
* <option key={loc.code} value={loc.code}>{loc.nativeName}</option>
|
||||
* ))}
|
||||
* </select>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function useLocale() {
|
||||
const localeService = useMemo(() => Core.services.resolve(LocaleService), []);
|
||||
const [locale, setLocale] = useState<Locale>(() => localeService.getCurrentLocale());
|
||||
@@ -14,17 +43,44 @@ export function useLocale() {
|
||||
return unsubscribe;
|
||||
}, [localeService]);
|
||||
|
||||
const t = useCallback((key: string, fallback?: string) => {
|
||||
return localeService.t(key, fallback);
|
||||
}, [localeService]);
|
||||
/**
|
||||
* 翻译函数
|
||||
* Translation function
|
||||
*
|
||||
* @param key - 翻译键 | Translation key
|
||||
* @param params - 可选参数,用于替换 {{key}} | Optional params for {{key}} substitution
|
||||
* @param fallback - 回退文本 | Fallback text
|
||||
*/
|
||||
const t = useCallback(
|
||||
(key: string, params?: TranslationParams, fallback?: string) => {
|
||||
return localeService.t(key, params, fallback);
|
||||
},
|
||||
[localeService]
|
||||
);
|
||||
|
||||
const changeLocale = useCallback((newLocale: Locale) => {
|
||||
localeService.setLocale(newLocale);
|
||||
/**
|
||||
* 切换语言
|
||||
* Change locale
|
||||
*/
|
||||
const changeLocale = useCallback(
|
||||
(newLocale: Locale) => {
|
||||
localeService.setLocale(newLocale);
|
||||
},
|
||||
[localeService]
|
||||
);
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* Get supported locales
|
||||
*/
|
||||
const supportedLocales: readonly LocaleInfo[] = useMemo(() => {
|
||||
return localeService.getSupportedLocales();
|
||||
}, [localeService]);
|
||||
|
||||
return {
|
||||
locale,
|
||||
t,
|
||||
changeLocale
|
||||
changeLocale,
|
||||
supportedLocales
|
||||
};
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1140
packages/editor-app/src/locales/es.ts
Normal file
1140
packages/editor-app/src/locales/es.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1,10 @@
|
||||
/**
|
||||
* Editor Core Translations
|
||||
* 编辑器核心翻译
|
||||
*
|
||||
* 插件翻译请使用 LocaleService.extendTranslations() 注册
|
||||
* Plugin translations should be registered via LocaleService.extendTranslations()
|
||||
*/
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,22 +18,24 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
const settingsRegistry = services.resolve(SettingsRegistry);
|
||||
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'appearance',
|
||||
title: '外观',
|
||||
description: '配置编辑器的外观设置',
|
||||
title: '$pluginSettings.appearance.title',
|
||||
description: '$pluginSettings.appearance.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'font',
|
||||
title: '字体设置',
|
||||
description: '配置编辑器字体样式',
|
||||
title: '$pluginSettings.appearance.font.title',
|
||||
description: '$pluginSettings.appearance.font.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'editor.fontSize',
|
||||
label: '字体大小 (px)',
|
||||
label: '$pluginSettings.appearance.font.fontSize.label',
|
||||
type: 'range',
|
||||
defaultValue: 13,
|
||||
description: '编辑器界面的字体大小',
|
||||
description: '$pluginSettings.appearance.font.fontSize.description',
|
||||
min: 11,
|
||||
max: 18,
|
||||
step: 1
|
||||
@@ -42,15 +44,15 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
id: 'inspector',
|
||||
title: '检视器设置',
|
||||
description: '配置属性检视器显示',
|
||||
title: '$pluginSettings.appearance.inspector.title',
|
||||
description: '$pluginSettings.appearance.inspector.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'inspector.decimalPlaces',
|
||||
label: '数字小数位数',
|
||||
label: '$pluginSettings.appearance.inspector.decimalPlaces.label',
|
||||
type: 'number',
|
||||
defaultValue: 4,
|
||||
description: '数字类型属性显示的小数位数,设置为 -1 表示不限制',
|
||||
description: '$pluginSettings.appearance.inspector.decimalPlaces.description',
|
||||
min: -1,
|
||||
max: 10,
|
||||
step: 1
|
||||
@@ -59,15 +61,15 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
id: 'scriptEditor',
|
||||
title: '脚本编辑器',
|
||||
description: '配置用于打开脚本文件的外部编辑器',
|
||||
title: '$pluginSettings.appearance.scriptEditor.title',
|
||||
description: '$pluginSettings.appearance.scriptEditor.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'editor.scriptEditor',
|
||||
label: '脚本编辑器',
|
||||
label: '$pluginSettings.appearance.scriptEditor.editor.label',
|
||||
type: 'select',
|
||||
defaultValue: 'system',
|
||||
description: '双击脚本文件时使用的编辑器',
|
||||
description: '$pluginSettings.appearance.scriptEditor.editor.description',
|
||||
options: SettingsService.SCRIPT_EDITORS.map(editor => ({
|
||||
value: editor.id,
|
||||
label: editor.name
|
||||
@@ -75,11 +77,11 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
key: 'editor.customScriptEditorCommand',
|
||||
label: '自定义编辑器命令',
|
||||
label: '$pluginSettings.appearance.scriptEditor.customCommand.label',
|
||||
type: 'string',
|
||||
defaultValue: '',
|
||||
description: '当选择"自定义"时,填写编辑器的命令行命令(如 notepad++)',
|
||||
placeholder: '例如:notepad++'
|
||||
description: '$pluginSettings.appearance.scriptEditor.customCommand.description',
|
||||
placeholder: '$pluginSettings.appearance.scriptEditor.customCommand.placeholder'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,22 +17,24 @@ class PluginConfigEditorModule implements IEditorModuleLoader {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
const settingsRegistry = services.resolve(SettingsRegistry);
|
||||
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'plugins',
|
||||
title: '插件',
|
||||
description: '管理项目使用的插件',
|
||||
title: '$pluginSettings.plugins.title',
|
||||
description: '$pluginSettings.plugins.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'engine-plugins',
|
||||
title: '插件管理',
|
||||
description: '启用或禁用项目需要的插件。禁用不需要的插件可以减少打包体积。',
|
||||
title: '$pluginSettings.plugins.management.title',
|
||||
description: '$pluginSettings.plugins.management.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'project.enabledPlugins',
|
||||
label: '',
|
||||
label: '$pluginSettings.plugins.management.list.label',
|
||||
type: 'pluginList',
|
||||
defaultValue: [],
|
||||
description: ''
|
||||
description: '$pluginSettings.plugins.management.list.description'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,60 +26,62 @@ class ProfilerEditorModule implements IEditorModuleLoader {
|
||||
this.messageHub = services.resolve(MessageHub);
|
||||
|
||||
const settingsRegistry = services.resolve(SettingsRegistry);
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'profiler',
|
||||
title: '性能分析器',
|
||||
description: '配置性能分析器的行为和显示选项',
|
||||
title: '$pluginSettings.profiler.title',
|
||||
description: '$pluginSettings.profiler.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'connection',
|
||||
title: '连接设置',
|
||||
description: '配置WebSocket服务器连接参数',
|
||||
title: '$pluginSettings.profiler.connection.title',
|
||||
description: '$pluginSettings.profiler.connection.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'profiler.port',
|
||||
label: '监听端口',
|
||||
label: '$pluginSettings.profiler.connection.port.label',
|
||||
type: 'number',
|
||||
defaultValue: 8080,
|
||||
description: '性能分析器WebSocket服务器监听的端口号',
|
||||
placeholder: '8080',
|
||||
description: '$pluginSettings.profiler.connection.port.description',
|
||||
placeholder: '$pluginSettings.profiler.connection.port.placeholder',
|
||||
min: 1024,
|
||||
max: 65535,
|
||||
validator: {
|
||||
validate: (value: number) => value >= 1024 && value <= 65535,
|
||||
errorMessage: '端口号必须在1024到65535之间'
|
||||
errorMessage: '$pluginSettings.profiler.connection.port.errorMessage'
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'profiler.autoStart',
|
||||
label: '自动启动服务器',
|
||||
label: '$pluginSettings.profiler.connection.autoStart.label',
|
||||
type: 'boolean',
|
||||
defaultValue: true,
|
||||
description: '编辑器启动时自动启动性能分析器服务器'
|
||||
description: '$pluginSettings.profiler.connection.autoStart.description'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'display',
|
||||
title: '显示设置',
|
||||
description: '配置性能数据的显示选项',
|
||||
title: '$pluginSettings.profiler.display.title',
|
||||
description: '$pluginSettings.profiler.display.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'profiler.refreshInterval',
|
||||
label: '刷新间隔 (毫秒)',
|
||||
label: '$pluginSettings.profiler.display.refreshInterval.label',
|
||||
type: 'range',
|
||||
defaultValue: 100,
|
||||
description: '性能数据刷新的时间间隔',
|
||||
description: '$pluginSettings.profiler.display.refreshInterval.description',
|
||||
min: 50,
|
||||
max: 1000,
|
||||
step: 50
|
||||
},
|
||||
{
|
||||
key: 'profiler.maxDataPoints',
|
||||
label: '最大数据点数',
|
||||
label: '$pluginSettings.profiler.display.maxDataPoints.label',
|
||||
type: 'number',
|
||||
defaultValue: 100,
|
||||
description: '图表中保留的最大历史数据点数量',
|
||||
description: '$pluginSettings.profiler.display.maxDataPoints.description',
|
||||
min: 10,
|
||||
max: 500
|
||||
}
|
||||
|
||||
@@ -58,42 +58,46 @@ class ProjectSettingsEditorModule implements IEditorModuleLoader {
|
||||
// Setup listener for UI design resolution changes
|
||||
this.setupSettingsListener();
|
||||
|
||||
// Register settings using translation keys (prefixed with '$')
|
||||
// 使用翻译键注册设置(以 '$' 为前缀)
|
||||
settingsRegistry.registerCategory({
|
||||
id: 'project',
|
||||
title: '项目',
|
||||
description: '项目级别的配置',
|
||||
title: '$pluginSettings.project.title',
|
||||
description: '$pluginSettings.project.description',
|
||||
sections: [
|
||||
{
|
||||
id: 'ui-settings',
|
||||
title: 'UI 设置',
|
||||
description: '配置 UI 系统的基础参数',
|
||||
title: '$pluginSettings.project.uiSettings.title',
|
||||
description: '$pluginSettings.project.uiSettings.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'project.uiDesignResolution.width',
|
||||
label: '设计宽度',
|
||||
label: '$pluginSettings.project.uiSettings.designWidth.label',
|
||||
type: 'number',
|
||||
defaultValue: 1920,
|
||||
description: 'UI 画布的设计宽度(像素)',
|
||||
description: '$pluginSettings.project.uiSettings.designWidth.description',
|
||||
min: 320,
|
||||
max: 7680,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
key: 'project.uiDesignResolution.height',
|
||||
label: '设计高度',
|
||||
label: '$pluginSettings.project.uiSettings.designHeight.label',
|
||||
type: 'number',
|
||||
defaultValue: 1080,
|
||||
description: 'UI 画布的设计高度(像素)',
|
||||
description: '$pluginSettings.project.uiSettings.designHeight.description',
|
||||
min: 240,
|
||||
max: 4320,
|
||||
step: 1
|
||||
},
|
||||
{
|
||||
key: 'project.uiDesignResolution.preset',
|
||||
label: '分辨率预设',
|
||||
label: '$pluginSettings.project.uiSettings.resolutionPreset.label',
|
||||
type: 'select',
|
||||
defaultValue: '1920x1080',
|
||||
description: '选择常见的分辨率预设',
|
||||
description: '$pluginSettings.project.uiSettings.resolutionPreset.description',
|
||||
// Resolution preset options use static labels (not localized)
|
||||
// 分辨率预设选项使用静态标签(不本地化)
|
||||
options: UI_RESOLUTION_PRESETS.map(p => ({
|
||||
label: p.label,
|
||||
value: `${p.value.width}x${p.value.height}`
|
||||
@@ -103,17 +107,17 @@ class ProjectSettingsEditorModule implements IEditorModuleLoader {
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
title: '引擎模块',
|
||||
description: '管理项目使用的引擎模块。每个模块包含运行时组件和编辑器工具。禁用不需要的模块可以减小构建体积。',
|
||||
title: '$pluginSettings.project.modules.title',
|
||||
description: '$pluginSettings.project.modules.description',
|
||||
settings: [
|
||||
{
|
||||
key: 'project.disabledModules',
|
||||
label: '模块列表',
|
||||
label: '$pluginSettings.project.modules.list.label',
|
||||
type: 'moduleList',
|
||||
// Default: no modules disabled (all enabled)
|
||||
// 默认:没有禁用的模块(全部启用)
|
||||
defaultValue: [],
|
||||
description: '取消勾选不需要的模块。核心模块不能禁用。新增的模块会自动启用。',
|
||||
description: '$pluginSettings.project.modules.list.description',
|
||||
// Custom props for moduleList type
|
||||
// Modules are loaded dynamically from ModuleRegistry (sizes from module.json)
|
||||
// 模块从 ModuleRegistry 动态加载(大小来自 module.json)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useRef, useEffect, useState } from 'react';
|
||||
import { NodeTemplate, NodeTemplates } from '@esengine/behavior-tree';
|
||||
import { Search, X, LucideIcon, ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
|
||||
interface QuickCreateMenuProps {
|
||||
visible: boolean;
|
||||
@@ -32,6 +33,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
onNodeSelect,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const selectedNodeRef = useRef<HTMLDivElement>(null);
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(false);
|
||||
@@ -48,11 +50,12 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
})
|
||||
: allTemplates;
|
||||
|
||||
const uncategorizedLabel = t('quickCreateMenu.uncategorized');
|
||||
const categoryGroups: CategoryGroup[] = React.useMemo(() => {
|
||||
const groups = new Map<string, NodeTemplate[]>();
|
||||
|
||||
filteredTemplates.forEach((template: NodeTemplate) => {
|
||||
const category = template.category || '未分类';
|
||||
const category = template.category || uncategorizedLabel;
|
||||
if (!groups.has(category)) {
|
||||
groups.set(category, []);
|
||||
}
|
||||
@@ -64,7 +67,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
templates,
|
||||
isExpanded: searchTextLower ? true : expandedCategories.has(category)
|
||||
})).sort((a, b) => a.category.localeCompare(b.category));
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower]);
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower, uncategorizedLabel]);
|
||||
|
||||
const flattenedTemplates = React.useMemo(() => {
|
||||
return categoryGroups.flatMap((group) =>
|
||||
@@ -86,10 +89,10 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (allTemplates.length > 0 && expandedCategories.size === 0) {
|
||||
const categories = new Set(allTemplates.map((t) => t.category || '未分类'));
|
||||
const categories = new Set(allTemplates.map((template) => template.category || uncategorizedLabel));
|
||||
setExpandedCategories(categories);
|
||||
}
|
||||
}, [allTemplates, expandedCategories.size]);
|
||||
}, [allTemplates, expandedCategories.size, uncategorizedLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoScroll && selectedNodeRef.current) {
|
||||
@@ -157,7 +160,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
<Search size={16} style={{ color: '#999', flexShrink: 0 }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索节点..."
|
||||
placeholder={t('quickCreateMenu.searchPlaceholder')}
|
||||
autoFocus
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
@@ -225,7 +228,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
color: '#666',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
未找到匹配的节点
|
||||
{t('quickCreateMenu.noMatchingNodes')}
|
||||
</div>
|
||||
) : (
|
||||
categoryGroups.map((group) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Play, Pause, Square, SkipForward, RotateCcw, Trash2, Undo, Redo, Box } from 'lucide-react';
|
||||
import { useLocale } from '../../../hooks/useLocale';
|
||||
|
||||
type ExecutionMode = 'idle' | 'running' | 'paused' | 'step';
|
||||
|
||||
@@ -36,6 +37,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
onClearCanvas,
|
||||
onToggleGizmos
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -67,12 +69,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="运行 (Play)"
|
||||
title={t('editorToolbar.play')}
|
||||
>
|
||||
<Play size={16} />
|
||||
</button>
|
||||
|
||||
{/* 暂停按钮 */}
|
||||
{/* 暂停按钮 | Pause button */}
|
||||
<button
|
||||
onClick={onPause}
|
||||
disabled={executionMode === 'idle'}
|
||||
@@ -88,12 +90,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title={executionMode === 'paused' ? '继续' : '暂停'}
|
||||
title={executionMode === 'paused' ? t('editorToolbar.resume') : t('editorToolbar.pause')}
|
||||
>
|
||||
{executionMode === 'paused' ? <Play size={16} /> : <Pause size={16} />}
|
||||
</button>
|
||||
|
||||
{/* 停止按钮 */}
|
||||
{/* 停止按钮 | Stop button */}
|
||||
<button
|
||||
onClick={onStop}
|
||||
disabled={executionMode === 'idle'}
|
||||
@@ -109,12 +111,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="停止"
|
||||
title={t('editorToolbar.stop')}
|
||||
>
|
||||
<Square size={16} />
|
||||
</button>
|
||||
|
||||
{/* 单步执行按钮 */}
|
||||
{/* 单步执行按钮 | Step forward button */}
|
||||
<button
|
||||
onClick={onStep}
|
||||
disabled={executionMode !== 'idle' && executionMode !== 'paused'}
|
||||
@@ -130,12 +132,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="单步执行"
|
||||
title={t('editorToolbar.stepForward')}
|
||||
>
|
||||
<SkipForward size={16} />
|
||||
</button>
|
||||
|
||||
{/* 重置按钮 */}
|
||||
{/* 重置按钮 | Reset button */}
|
||||
<button
|
||||
onClick={onReset}
|
||||
style={{
|
||||
@@ -150,7 +152,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="重置"
|
||||
title={t('editorToolbar.reset')}
|
||||
>
|
||||
<RotateCcw size={16} />
|
||||
</button>
|
||||
@@ -162,7 +164,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
margin: '4px 0'
|
||||
}} />
|
||||
|
||||
{/* 重置视图按钮 */}
|
||||
{/* 重置视图按钮 | Reset view button */}
|
||||
<button
|
||||
onClick={onResetView}
|
||||
style={{
|
||||
@@ -177,13 +179,13 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
title="重置视图 (滚轮缩放, Alt+拖动平移)"
|
||||
title={t('editorToolbar.resetView')}
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
View
|
||||
</button>
|
||||
|
||||
{/* 清空画布按钮 */}
|
||||
{/* 清空画布按钮 | Clear canvas button */}
|
||||
<button
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
@@ -197,14 +199,14 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
title="清空画布"
|
||||
title={t('editorToolbar.clearCanvas')}
|
||||
onClick={onClearCanvas}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
清空
|
||||
{t('editorToolbar.clear')}
|
||||
</button>
|
||||
|
||||
{/* Gizmo 开关按钮 */}
|
||||
{/* Gizmo 开关按钮 | Gizmo toggle button */}
|
||||
<button
|
||||
onClick={onToggleGizmos}
|
||||
style={{
|
||||
@@ -219,7 +221,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}
|
||||
title="显示/隐藏选择边框 (Gizmos)"
|
||||
title={t('editorToolbar.toggleGizmos')}
|
||||
>
|
||||
<Box size={14} />
|
||||
Gizmos
|
||||
@@ -233,7 +235,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
margin: '0 4px'
|
||||
}} />
|
||||
|
||||
{/* 撤销按钮 */}
|
||||
{/* 撤销按钮 | Undo button */}
|
||||
<button
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
@@ -249,12 +251,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="撤销 (Ctrl+Z)"
|
||||
title={t('editorToolbar.undo')}
|
||||
>
|
||||
<Undo size={16} />
|
||||
</button>
|
||||
|
||||
{/* 重做按钮 */}
|
||||
{/* 重做按钮 | Redo button */}
|
||||
<button
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
@@ -270,12 +272,12 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
|
||||
title={t('editorToolbar.redo')}
|
||||
>
|
||||
<Redo size={16} />
|
||||
</button>
|
||||
|
||||
{/* 状态指示器 */}
|
||||
{/* 状态指示器 | Status indicator */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
@@ -294,9 +296,9 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
executionMode === 'running' ? '#4caf50' :
|
||||
executionMode === 'paused' ? '#ff9800' : '#666'
|
||||
}} />
|
||||
{executionMode === 'idle' ? 'Idle' :
|
||||
executionMode === 'running' ? 'Running' :
|
||||
executionMode === 'paused' ? 'Paused' : 'Step'}
|
||||
{executionMode === 'idle' ? t('editorToolbar.idle') :
|
||||
executionMode === 'running' ? t('editorToolbar.running') :
|
||||
executionMode === 'paused' ? t('editorToolbar.paused') : t('editorToolbar.step')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import type { ServiceToken } from '@esengine/engine-core';
|
||||
import {
|
||||
EntityStoreService,
|
||||
MessageHub,
|
||||
@@ -71,8 +72,13 @@ export interface IPluginAPI {
|
||||
getEntityStore(): EntityStoreService;
|
||||
/** 获取 MessageHub | Get MessageHub */
|
||||
getMessageHub(): MessageHub;
|
||||
/** 解析服务 | Resolve service */
|
||||
resolveService<T>(serviceType: any): T;
|
||||
/**
|
||||
* 解析服务 | Resolve service
|
||||
*
|
||||
* 支持 ServiceToken<T>(推荐)或传统的 class/symbol。
|
||||
* Supports ServiceToken<T> (recommended) or legacy class/symbol.
|
||||
*/
|
||||
resolveService<T>(serviceType: ServiceToken<T> | symbol | (new (...args: any[]) => T)): T;
|
||||
/** 获取 Core 实例 | Get Core instance */
|
||||
getCore(): typeof Core;
|
||||
}
|
||||
@@ -185,7 +191,16 @@ export class PluginSDKRegistry {
|
||||
}
|
||||
return messageHubInstance;
|
||||
},
|
||||
resolveService: <T>(serviceType: any): T => Core.services.resolve(serviceType) as T,
|
||||
resolveService: <T>(serviceType: ServiceToken<T> | symbol | (new (...args: any[]) => T)): T => {
|
||||
// 检测是否是 ServiceToken(具有 id: symbol 属性)
|
||||
// Detect if this is a ServiceToken (has id: symbol property)
|
||||
if (serviceType && typeof serviceType === 'object' && 'id' in serviceType && typeof serviceType.id === 'symbol') {
|
||||
return Core.services.resolve(serviceType.id) as T;
|
||||
}
|
||||
// 传统方式:直接使用 class 或 symbol
|
||||
// Legacy: use class or symbol directly
|
||||
return Core.services.resolve(serviceType as symbol) as T;
|
||||
},
|
||||
getCore: () => Core,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,14 +93,17 @@ export class SettingsService {
|
||||
/**
|
||||
* 支持的脚本编辑器类型
|
||||
* Supported script editor types
|
||||
*
|
||||
* 使用 nameKey 作为翻译键,如果没有 nameKey 则使用 name 作为显示名称
|
||||
* Use nameKey as translation key, fallback to name if no nameKey
|
||||
*/
|
||||
public static readonly SCRIPT_EDITORS = [
|
||||
{ id: 'system', name: 'System Default', nameZh: '系统默认', command: '' },
|
||||
{ id: 'vscode', name: 'Visual Studio Code', nameZh: 'Visual Studio Code', command: 'code' },
|
||||
{ id: 'cursor', name: 'Cursor', nameZh: 'Cursor', command: 'cursor' },
|
||||
{ id: 'webstorm', name: 'WebStorm', nameZh: 'WebStorm', command: 'webstorm' },
|
||||
{ id: 'sublime', name: 'Sublime Text', nameZh: 'Sublime Text', command: 'subl' },
|
||||
{ id: 'custom', name: 'Custom', nameZh: '自定义', command: '' }
|
||||
{ id: 'system', name: 'System Default', nameKey: 'settings.scriptEditor.systemDefault', command: '' },
|
||||
{ id: 'vscode', name: 'Visual Studio Code', command: 'code' },
|
||||
{ id: 'cursor', name: 'Cursor', command: 'cursor' },
|
||||
{ id: 'webstorm', name: 'WebStorm', command: 'webstorm' },
|
||||
{ id: 'sublime', name: 'Sublime Text', command: 'subl' },
|
||||
{ id: 'custom', name: 'Custom', nameKey: 'settings.scriptEditor.custom', command: '' }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,14 @@ export interface IDialogExtended extends IDialog {
|
||||
setLocale(locale: string): void;
|
||||
}
|
||||
|
||||
const dialogTranslations = {
|
||||
en: { confirm: 'Confirm', cancel: 'Cancel' },
|
||||
zh: { confirm: '确定', cancel: '取消' },
|
||||
es: { confirm: 'Confirmar', cancel: 'Cancelar' }
|
||||
} as const;
|
||||
|
||||
type LocaleKey = keyof typeof dialogTranslations;
|
||||
|
||||
@singleton()
|
||||
export class TauriDialogService implements IDialogExtended {
|
||||
private showConfirmCallback?: (data: ConfirmDialogData) => void;
|
||||
@@ -70,8 +78,10 @@ export class TauriDialogService implements IDialogExtended {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmText = this.locale === 'zh' ? '确定' : 'Confirm';
|
||||
const cancelText = this.locale === 'zh' ? '取消' : 'Cancel';
|
||||
const localeKey = (this.locale in dialogTranslations ? this.locale : 'en') as LocaleKey;
|
||||
const texts = dialogTranslations[localeKey];
|
||||
const confirmText = texts.confirm;
|
||||
const cancelText = texts.cancel;
|
||||
|
||||
this.showConfirmCallback({
|
||||
title,
|
||||
|
||||
@@ -45,10 +45,19 @@ export enum PanelPosition {
|
||||
export interface PanelDescriptor {
|
||||
/** 面板ID | Panel ID */
|
||||
id: string;
|
||||
/** 面板标题 | Panel title */
|
||||
/**
|
||||
* 面板标题 | Panel title
|
||||
* 作为默认/英文标题,当 titleKey 未设置或翻译缺失时使用
|
||||
* Used as default/English title when titleKey is not set or translation is missing
|
||||
*/
|
||||
title: string;
|
||||
/** 面板中文标题 | Panel title in Chinese */
|
||||
titleZh?: string;
|
||||
/**
|
||||
* 面板标题翻译键 | Panel title translation key
|
||||
* 设置后会根据当前语言自动翻译
|
||||
* When set, title will be automatically translated based on current locale
|
||||
* @example 'panel.behaviorTreeEditor'
|
||||
*/
|
||||
titleKey?: string;
|
||||
/** 面板图标 | Panel icon */
|
||||
icon?: string;
|
||||
/** 面板位置 | Panel position */
|
||||
|
||||
@@ -4,16 +4,90 @@ import { createLogger } from '@esengine/ecs-framework';
|
||||
|
||||
const logger = createLogger('LocaleService');
|
||||
|
||||
export type Locale = 'en' | 'zh';
|
||||
/**
|
||||
* 支持的语言类型
|
||||
* Supported locale types
|
||||
*
|
||||
* - en: English
|
||||
* - zh: 简体中文 (Simplified Chinese)
|
||||
* - es: Español (Spanish)
|
||||
*/
|
||||
export type Locale = 'en' | 'zh' | 'es';
|
||||
|
||||
/**
|
||||
* 语言显示信息
|
||||
* Locale display information
|
||||
*/
|
||||
export interface LocaleInfo {
|
||||
code: Locale;
|
||||
name: string;
|
||||
nativeName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的语言列表
|
||||
* List of supported locales
|
||||
*/
|
||||
export const SUPPORTED_LOCALES: readonly LocaleInfo[] = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'zh', name: 'Chinese', nativeName: '简体中文' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' }
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 翻译值类型
|
||||
* Translation value type
|
||||
*/
|
||||
export interface Translations {
|
||||
[key: string]: string | Translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化服务
|
||||
* 插件翻译包
|
||||
* Plugin translation bundle
|
||||
*
|
||||
* 管理编辑器的多语言支持
|
||||
* 用于插件注册自己的翻译
|
||||
* Used for plugins to register their own translations
|
||||
*/
|
||||
export interface PluginTranslations {
|
||||
en: Translations;
|
||||
zh: Translations;
|
||||
es?: Translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译参数类型
|
||||
* Translation parameters type
|
||||
*/
|
||||
export type TranslationParams = Record<string, string | number>;
|
||||
|
||||
/**
|
||||
* 国际化服务
|
||||
* Internationalization service
|
||||
*
|
||||
* 管理编辑器的多语言支持,提供翻译、语言切换和事件通知功能。
|
||||
* Manages editor's multi-language support, provides translation, locale switching and event notification.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取服务 | Get service
|
||||
* const localeService = Core.services.resolve(LocaleService);
|
||||
*
|
||||
* // 翻译文本 | Translate text
|
||||
* localeService.t('common.save'); // "Save" or "保存"
|
||||
*
|
||||
* // 带参数的翻译 | Translation with parameters
|
||||
* localeService.t('scene.savedSuccess', { name: 'MyScene' }); // "Scene saved: MyScene"
|
||||
*
|
||||
* // 切换语言 | Switch locale
|
||||
* localeService.setLocale('zh');
|
||||
*
|
||||
* // 插件注册翻译 | Plugin register translations
|
||||
* localeService.extendTranslations('behaviorTree', {
|
||||
* en: { title: 'Behavior Tree Editor', ... },
|
||||
* zh: { title: '行为树编辑器', ... }
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class LocaleService implements IService {
|
||||
@@ -29,22 +103,125 @@ export class LocaleService implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册语言包
|
||||
* 注册核心语言包(覆盖式)
|
||||
* Register core translations (overwrites existing)
|
||||
*
|
||||
* 用于编辑器核心初始化时注册基础翻译
|
||||
* Used for editor core to register base translations during initialization
|
||||
*
|
||||
* @param locale - 语言代码 | Locale code
|
||||
* @param translations - 翻译对象 | Translation object
|
||||
*/
|
||||
public registerTranslations(locale: Locale, translations: Translations): void {
|
||||
this.translations.set(locale, translations);
|
||||
logger.info(`Registered translations for locale: ${locale}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展语言包(合并式)
|
||||
* Extend translations (merges with existing)
|
||||
*
|
||||
* 用于插件注册自己的翻译,会合并到现有翻译中
|
||||
* Used for plugins to register their translations, merges with existing
|
||||
*
|
||||
* @param namespace - 命名空间,如 'behaviorTree' | Namespace, e.g. 'behaviorTree'
|
||||
* @param pluginTranslations - 插件翻译包 | Plugin translation bundle
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 在插件的 editorModule.install() 中调用
|
||||
* // Call in plugin's editorModule.install()
|
||||
* localeService.extendTranslations('behaviorTree', {
|
||||
* en: {
|
||||
* title: 'Behavior Tree Editor',
|
||||
* nodePalette: 'Node Palette',
|
||||
* // ...
|
||||
* },
|
||||
* zh: {
|
||||
* title: '行为树编辑器',
|
||||
* nodePalette: '节点面板',
|
||||
* // ...
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // 然后在组件中使用
|
||||
* // Then use in components
|
||||
* t('behaviorTree.title') // "Behavior Tree Editor" or "行为树编辑器"
|
||||
* ```
|
||||
*/
|
||||
public extendTranslations(namespace: string, pluginTranslations: PluginTranslations): void {
|
||||
const locales: Locale[] = ['en', 'zh', 'es'];
|
||||
|
||||
for (const locale of locales) {
|
||||
const existing = this.translations.get(locale) || {};
|
||||
const pluginTrans = pluginTranslations[locale];
|
||||
|
||||
if (pluginTrans) {
|
||||
// 深度合并到命名空间下 | Deep merge under namespace
|
||||
const merged = {
|
||||
...existing,
|
||||
[namespace]: this.deepMerge(
|
||||
(existing[namespace] as Translations) || {},
|
||||
pluginTrans
|
||||
)
|
||||
};
|
||||
this.translations.set(locale, merged);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Extended translations for namespace: ${namespace}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度合并两个翻译对象
|
||||
* Deep merge two translation objects
|
||||
*/
|
||||
private deepMerge(target: Translations, source: Translations): Translations {
|
||||
const result: Translations = { ...target };
|
||||
|
||||
for (const key of Object.keys(source)) {
|
||||
const sourceValue = source[key];
|
||||
const targetValue = target[key];
|
||||
|
||||
if (
|
||||
typeof sourceValue === 'object' &&
|
||||
sourceValue !== null &&
|
||||
typeof targetValue === 'object' &&
|
||||
targetValue !== null
|
||||
) {
|
||||
result[key] = this.deepMerge(
|
||||
targetValue as Translations,
|
||||
sourceValue as Translations
|
||||
);
|
||||
} else {
|
||||
result[key] = sourceValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
* Get current locale
|
||||
*/
|
||||
public getCurrentLocale(): Locale {
|
||||
return this.currentLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* Get list of supported locales
|
||||
*/
|
||||
public getSupportedLocales(): readonly LocaleInfo[] {
|
||||
return SUPPORTED_LOCALES;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前语言
|
||||
* Set current locale
|
||||
*
|
||||
* @param locale - 目标语言代码 | Target locale code
|
||||
*/
|
||||
public setLocale(locale: Locale): void {
|
||||
if (!this.translations.has(locale)) {
|
||||
@@ -62,11 +239,28 @@ export class LocaleService implements IService {
|
||||
|
||||
/**
|
||||
* 翻译文本
|
||||
* Translate text
|
||||
*
|
||||
* @param key - 翻译键,支持点分隔的路径如 "menu.file.save"
|
||||
* @param fallback - 如果找不到翻译时的回退文本
|
||||
* @param key - 翻译键,支持点分隔的路径 | Translation key, supports dot-separated paths
|
||||
* @param params - 可选的参数对象,用于替换模板中的占位符 {{key}} | Optional params for placeholder substitution
|
||||
* @param fallback - 如果找不到翻译时的回退文本 | Fallback text if translation not found
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 简单翻译 | Simple translation
|
||||
* t('common.save') // "Save"
|
||||
*
|
||||
* // 带参数替换 | With parameter substitution
|
||||
* t('scene.savedSuccess', { name: 'MyScene' }) // "Scene saved: MyScene"
|
||||
*
|
||||
* // 插件翻译 | Plugin translation
|
||||
* t('behaviorTree.title') // "Behavior Tree Editor"
|
||||
*
|
||||
* // 带回退文本 | With fallback
|
||||
* t('unknown.key', undefined, 'Default Text') // "Default Text"
|
||||
* ```
|
||||
*/
|
||||
public t(key: string, fallback?: string): string {
|
||||
public t(key: string, params?: TranslationParams, fallback?: string): string {
|
||||
const translations = this.translations.get(this.currentLocale);
|
||||
if (!translations) {
|
||||
return fallback || key;
|
||||
@@ -74,6 +268,12 @@ export class LocaleService implements IService {
|
||||
|
||||
const value = this.getNestedValue(translations, key);
|
||||
if (typeof value === 'string') {
|
||||
// 支持参数替换 {{key}} | Support parameter substitution {{key}}
|
||||
if (params) {
|
||||
return value.replace(/\{\{(\w+)\}\}/g, (_, paramKey) => {
|
||||
return String(params[paramKey] ?? `{{${paramKey}}}`);
|
||||
});
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -82,6 +282,10 @@ export class LocaleService implements IService {
|
||||
|
||||
/**
|
||||
* 监听语言变化
|
||||
* Listen to locale changes
|
||||
*
|
||||
* @param listener - 回调函数 | Callback function
|
||||
* @returns 取消订阅函数 | Unsubscribe function
|
||||
*/
|
||||
public onChange(listener: (locale: Locale) => void): () => void {
|
||||
this.changeListeners.add(listener);
|
||||
@@ -91,12 +295,30 @@ export class LocaleService implements IService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查翻译键是否存在
|
||||
* Check if a translation key exists
|
||||
*
|
||||
* @param key - 翻译键 | Translation key
|
||||
* @param locale - 可选的语言代码,默认使用当前语言 | Optional locale, defaults to current
|
||||
*/
|
||||
public hasKey(key: string, locale?: Locale): boolean {
|
||||
const targetLocale = locale || this.currentLocale;
|
||||
const translations = this.translations.get(targetLocale);
|
||||
if (!translations) {
|
||||
return false;
|
||||
}
|
||||
const value = this.getNestedValue(translations, key);
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取嵌套对象的值
|
||||
* Get nested object value
|
||||
*/
|
||||
private getNestedValue(obj: Translations, path: string): string | Translations | undefined {
|
||||
const keys = path.split('.');
|
||||
let current: any = obj;
|
||||
let current: string | Translations | undefined = obj;
|
||||
|
||||
for (const key of keys) {
|
||||
if (current && typeof current === 'object' && key in current) {
|
||||
@@ -111,11 +333,12 @@ export class LocaleService implements IService {
|
||||
|
||||
/**
|
||||
* 从 localStorage 加载保存的语言设置
|
||||
* Load saved locale from localStorage
|
||||
*/
|
||||
private loadSavedLocale(): Locale | null {
|
||||
try {
|
||||
const saved = localStorage.getItem('editor-locale');
|
||||
if (saved === 'en' || saved === 'zh') {
|
||||
if (saved === 'en' || saved === 'zh' || saved === 'es') {
|
||||
return saved;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -126,6 +349,7 @@ export class LocaleService implements IService {
|
||||
|
||||
/**
|
||||
* 保存语言设置到 localStorage
|
||||
* Save locale to localStorage
|
||||
*/
|
||||
private saveLocale(locale: Locale): void {
|
||||
try {
|
||||
|
||||
@@ -2,44 +2,83 @@ import { Injectable, IService } from '@esengine/ecs-framework';
|
||||
|
||||
export type SettingType = 'string' | 'number' | 'boolean' | 'select' | 'color' | 'range' | 'pluginList' | 'collisionMatrix' | 'moduleList';
|
||||
|
||||
/**
|
||||
* Localizable text - can be a plain string or a translation key (prefixed with '$')
|
||||
* 可本地化文本 - 可以是普通字符串或翻译键(以 '$' 为前缀)
|
||||
*
|
||||
* @example
|
||||
* // Plain text (not recommended for user-facing strings)
|
||||
* title: 'Appearance'
|
||||
*
|
||||
* // Translation key (recommended)
|
||||
* title: '$pluginSettings.appearance.title'
|
||||
*/
|
||||
export type LocalizableText = string;
|
||||
|
||||
/**
|
||||
* Check if text is a translation key (starts with '$')
|
||||
* 检查文本是否为翻译键(以 '$' 开头)
|
||||
*/
|
||||
export function isTranslationKey(text: string): boolean {
|
||||
return text.startsWith('$');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual translation key (without '$' prefix)
|
||||
* 获取实际的翻译键(去掉 '$' 前缀)
|
||||
*/
|
||||
export function getTranslationKey(text: string): string {
|
||||
return text.startsWith('$') ? text.slice(1) : text;
|
||||
}
|
||||
|
||||
export interface SettingOption {
|
||||
label: string;
|
||||
label: LocalizableText;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface SettingValidator {
|
||||
validate: (value: any) => boolean;
|
||||
errorMessage: string;
|
||||
errorMessage: LocalizableText;
|
||||
}
|
||||
|
||||
export interface SettingDescriptor {
|
||||
key: string;
|
||||
label: string;
|
||||
/** Label text or translation key (prefixed with '$') | 标签文本或翻译键(以 '$' 为前缀) */
|
||||
label: LocalizableText;
|
||||
type: SettingType;
|
||||
defaultValue: any;
|
||||
description?: string;
|
||||
placeholder?: string;
|
||||
/** Description text or translation key (prefixed with '$') | 描述文本或翻译键(以 '$' 为前缀) */
|
||||
description?: LocalizableText;
|
||||
/** Placeholder text or translation key (prefixed with '$') | 占位符文本或翻译键(以 '$' 为前缀) */
|
||||
placeholder?: LocalizableText;
|
||||
options?: SettingOption[];
|
||||
validator?: SettingValidator;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
/** 自定义渲染器组件(用于 collisionMatrix 等复杂类型) */
|
||||
/**
|
||||
* Custom renderer component (for complex types like collisionMatrix)
|
||||
* 自定义渲染器组件(用于 collisionMatrix 等复杂类型)
|
||||
*/
|
||||
customRenderer?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface SettingSection {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
/** Title text or translation key (prefixed with '$') | 标题文本或翻译键(以 '$' 为前缀) */
|
||||
title: LocalizableText;
|
||||
/** Description text or translation key (prefixed with '$') | 描述文本或翻译键(以 '$' 为前缀) */
|
||||
description?: LocalizableText;
|
||||
icon?: string;
|
||||
settings: SettingDescriptor[];
|
||||
}
|
||||
|
||||
export interface SettingCategory {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
/** Title text or translation key (prefixed with '$') | 标题文本或翻译键(以 '$' 为前缀) */
|
||||
title: LocalizableText;
|
||||
/** Description text or translation key (prefixed with '$') | 描述文本或翻译键(以 '$' 为前缀) */
|
||||
description?: LocalizableText;
|
||||
sections: SettingSection[];
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
* Plugin-based editor framework for ECS Framework
|
||||
*/
|
||||
|
||||
// Service Tokens | 服务令牌
|
||||
export * from './tokens';
|
||||
|
||||
// 配置 | Configuration
|
||||
export * from './Config';
|
||||
|
||||
|
||||
157
packages/editor-core/src/tokens.ts
Normal file
157
packages/editor-core/src/tokens.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Editor Core 服务令牌
|
||||
* Editor Core service tokens
|
||||
*
|
||||
* 定义 editor-core 模块导出的服务令牌和接口。
|
||||
* Defines service tokens and interfaces exported by editor-core module.
|
||||
*
|
||||
* 遵循 "谁定义接口,谁导出 Token" 的规范。
|
||||
* Follows the "who defines interface, who exports token" principle.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 消费方导入 Token | Consumer imports Token
|
||||
* import { LocaleServiceToken, MessageHubToken, EntityStoreServiceToken } from '@esengine/editor-core';
|
||||
*
|
||||
* // 获取服务 | Get service
|
||||
* const localeService = context.services.get(LocaleServiceToken);
|
||||
* const messageHub = context.services.get(MessageHubToken);
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/engine-core';
|
||||
import type { LocaleService, Locale, TranslationParams, PluginTranslations } from './Services/LocaleService';
|
||||
import type { MessageHub, MessageHandler, RequestHandler } from './Services/MessageHub';
|
||||
import type { EntityStoreService, EntityTreeNode } from './Services/EntityStoreService';
|
||||
|
||||
// ============================================================================
|
||||
// LocaleService Token
|
||||
// 国际化服务令牌
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* LocaleService 接口
|
||||
* LocaleService interface
|
||||
*
|
||||
* 提供类型安全的服务访问接口。
|
||||
* Provides type-safe service access interface.
|
||||
*/
|
||||
export interface ILocaleService {
|
||||
/** 获取当前语言 | Get current locale */
|
||||
getLocale(): Locale;
|
||||
/** 设置当前语言 | Set current locale */
|
||||
setLocale(locale: Locale): void;
|
||||
/** 翻译文本 | Translate text */
|
||||
t(key: string, params?: TranslationParams, fallback?: string): string;
|
||||
/** 扩展翻译 | Extend translations */
|
||||
extendTranslations(namespace: string, translations: PluginTranslations): void;
|
||||
/** 监听语言变化 | Listen to locale changes */
|
||||
onLocaleChange(listener: (locale: Locale) => void): () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化服务令牌
|
||||
* Localization service token
|
||||
*
|
||||
* 用于注册和获取国际化服务。
|
||||
* For registering and getting localization service.
|
||||
*/
|
||||
export const LocaleServiceToken = createServiceToken<ILocaleService>('localeService');
|
||||
|
||||
// ============================================================================
|
||||
// MessageHub Token
|
||||
// 消息总线令牌
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* MessageHub 服务接口
|
||||
* MessageHub service interface
|
||||
*
|
||||
* 提供类型安全的消息通信接口。
|
||||
* Provides type-safe message communication interface.
|
||||
*/
|
||||
export interface IMessageHubService {
|
||||
/** 订阅消息 | Subscribe to message */
|
||||
subscribe<T = unknown>(topic: string, handler: MessageHandler<T>): () => void;
|
||||
/** 订阅一次性消息 | Subscribe to one-time message */
|
||||
subscribeOnce<T = unknown>(topic: string, handler: MessageHandler<T>): () => void;
|
||||
/** 发布消息 | Publish message */
|
||||
publish<T = unknown>(topic: string, data?: T): Promise<void>;
|
||||
/** 注册请求处理器 | Register request handler */
|
||||
registerRequest<TRequest = unknown, TResponse = unknown>(
|
||||
topic: string,
|
||||
handler: RequestHandler<TRequest, TResponse>
|
||||
): () => void;
|
||||
/** 发送请求 | Send request */
|
||||
request<TRequest = unknown, TResponse = unknown>(
|
||||
topic: string,
|
||||
data?: TRequest,
|
||||
timeout?: number
|
||||
): Promise<TResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息总线服务令牌
|
||||
* Message hub service token
|
||||
*
|
||||
* 用于注册和获取消息总线服务。
|
||||
* For registering and getting message hub service.
|
||||
*/
|
||||
export const MessageHubToken = createServiceToken<IMessageHubService>('messageHub');
|
||||
|
||||
// ============================================================================
|
||||
// EntityStoreService Token
|
||||
// 实体存储服务令牌
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* EntityStoreService 接口
|
||||
* EntityStoreService interface
|
||||
*
|
||||
* 提供类型安全的实体存储服务访问接口。
|
||||
* Provides type-safe entity store service access interface.
|
||||
*/
|
||||
export interface IEntityStoreService {
|
||||
/** 添加实体 | Add entity */
|
||||
addEntity(entity: unknown, parent?: unknown): void;
|
||||
/** 移除实体 | Remove entity */
|
||||
removeEntity(entity: unknown): void;
|
||||
/** 选择实体 | Select entity */
|
||||
selectEntity(entity: unknown | null): void;
|
||||
/** 获取选中的实体 | Get selected entity */
|
||||
getSelectedEntity(): unknown | null;
|
||||
/** 获取所有实体 | Get all entities */
|
||||
getAllEntities(): unknown[];
|
||||
/** 获取根实体 | Get root entities */
|
||||
getRootEntities(): unknown[];
|
||||
/** 根据ID获取实体 | Get entity by ID */
|
||||
getEntity(id: number): unknown | undefined;
|
||||
/** 清空实体 | Clear all entities */
|
||||
clear(): void;
|
||||
/** 构建实体树 | Build entity tree */
|
||||
buildEntityTree(): EntityTreeNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体存储服务令牌
|
||||
* Entity store service token
|
||||
*
|
||||
* 用于注册和获取实体存储服务。
|
||||
* For registering and getting entity store service.
|
||||
*/
|
||||
export const EntityStoreServiceToken = createServiceToken<IEntityStoreService>('entityStoreService');
|
||||
|
||||
// ============================================================================
|
||||
// Re-export types for convenience
|
||||
// 重新导出类型方便使用
|
||||
// ============================================================================
|
||||
|
||||
export type { Locale, TranslationParams, PluginTranslations } from './Services/LocaleService';
|
||||
export type { MessageHandler, RequestHandler } from './Services/MessageHub';
|
||||
export type { EntityTreeNode } from './Services/EntityStoreService';
|
||||
|
||||
// Re-export classes for direct use (backwards compatibility)
|
||||
// 重新导出类以供直接使用(向后兼容)
|
||||
export { LocaleService } from './Services/LocaleService';
|
||||
export { MessageHub } from './Services/MessageHub';
|
||||
export { EntityStoreService } from './Services/EntityStoreService';
|
||||
@@ -29,6 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/ui": "workspace:*",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
|
||||
@@ -1,29 +1,63 @@
|
||||
/**
|
||||
* Plugin API - 为插件提供简洁的访问接口
|
||||
* Plugin API - Provides simple access interface for plugins
|
||||
*
|
||||
* 使用方式:
|
||||
* 使用方式 | Usage:
|
||||
* ```typescript
|
||||
* import { PluginAPI } from '@esengine/editor-runtime';
|
||||
*
|
||||
* const scene = PluginAPI.scene;
|
||||
* const entityStore = PluginAPI.entityStore;
|
||||
* const messageHub = PluginAPI.messageHub;
|
||||
*
|
||||
* // 使用 ServiceToken 获取服务(类型安全)| Get service with ServiceToken (type-safe)
|
||||
* import { AssetManagerToken } from '@esengine/asset-system';
|
||||
* const assetManager = PluginAPI.resolve(AssetManagerToken);
|
||||
* ```
|
||||
*
|
||||
* 这个 API 会自动从全局 __ESENGINE__ 获取正确的实例,
|
||||
* 避免模块实例不一致的问题。
|
||||
* This API automatically gets correct instances from global __ESENGINE__,
|
||||
* avoiding module instance inconsistency issues.
|
||||
*/
|
||||
|
||||
import type { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import type { Scene, ServiceContainer } from '@esengine/ecs-framework';
|
||||
import type { ServiceToken } from '@esengine/engine-core';
|
||||
|
||||
// 内部 API 接口定义
|
||||
/**
|
||||
* 核心服务接口
|
||||
* Core service interface
|
||||
*
|
||||
* 定义内部 Core 提供的服务访问接口。
|
||||
* Defines service access interface provided by internal Core.
|
||||
*/
|
||||
interface ICoreServices {
|
||||
services: ServiceContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部 API 接口定义
|
||||
* Internal API interface definition
|
||||
*
|
||||
* 定义全局 __ESENGINE__.api 提供的方法。
|
||||
* Defines methods provided by global __ESENGINE__.api.
|
||||
*/
|
||||
interface IPluginAPIInternal {
|
||||
/** 获取当前场景 | Get current scene */
|
||||
getScene(): Scene | null;
|
||||
/** 获取实体存储服务 | Get entity store service */
|
||||
getEntityStore(): EntityStoreService;
|
||||
/** 获取消息总线 | Get message hub */
|
||||
getMessageHub(): MessageHub;
|
||||
resolveService<T>(serviceType: any): T;
|
||||
getCore(): any;
|
||||
/**
|
||||
* 解析服务(类型安全)
|
||||
* Resolve service (type-safe)
|
||||
* @param token 服务令牌 | Service token
|
||||
*/
|
||||
resolveService<T>(token: ServiceToken<T>): T;
|
||||
/** 获取核心实例 | Get core instance */
|
||||
getCore(): ICoreServices;
|
||||
}
|
||||
|
||||
// 声明全局类型
|
||||
@@ -93,11 +127,24 @@ export const PluginAPI = {
|
||||
},
|
||||
|
||||
/**
|
||||
* 解析服务
|
||||
* @param serviceType 服务类型
|
||||
* 解析服务(类型安全)
|
||||
* Resolve service (type-safe)
|
||||
*
|
||||
* 使用 ServiceToken 获取服务实例,提供完整的类型推断。
|
||||
* Use ServiceToken to get service instance with full type inference.
|
||||
*
|
||||
* @param token 服务令牌 | Service token
|
||||
* @returns 服务实例 | Service instance
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { AssetManagerToken } from '@esengine/asset-system';
|
||||
* const assetManager = PluginAPI.resolve(AssetManagerToken);
|
||||
* // assetManager 类型自动推断为 IAssetManager
|
||||
* ```
|
||||
*/
|
||||
resolve<T>(serviceType: any): T {
|
||||
return getInternalAPI().resolveService<T>(serviceType);
|
||||
resolve<T>(token: ServiceToken<T>): T {
|
||||
return getInternalAPI().resolveService<T>(token);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
238
packages/editor-runtime/src/i18n/createPluginLocale.ts
Normal file
238
packages/editor-runtime/src/i18n/createPluginLocale.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Plugin Locale Factory
|
||||
* 插件国际化工厂
|
||||
*
|
||||
* Provides utilities for plugins to create their own locale hooks
|
||||
* that integrate with the central LocaleService.
|
||||
*
|
||||
* 为插件提供创建本地化 hook 的工具函数,
|
||||
* 这些 hook 会与中央 LocaleService 集成。
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { LocaleService, type Locale, type TranslationParams } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Translation object structure
|
||||
* 翻译对象结构
|
||||
*/
|
||||
export type Translations = {
|
||||
[key: string]: string | Translations;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin translations bundle
|
||||
* 插件翻译包
|
||||
*/
|
||||
export interface PluginTranslationsBundle<T extends Translations = Translations> {
|
||||
en: T;
|
||||
zh: T;
|
||||
es?: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type of usePluginLocale hook
|
||||
* usePluginLocale hook 的返回类型
|
||||
*/
|
||||
export interface PluginLocaleHook {
|
||||
/** Translation function | 翻译函数 */
|
||||
t: (key: string, params?: TranslationParams) => string;
|
||||
/** Current locale | 当前语言 */
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get nested value from object by dot-separated path
|
||||
* 通过点分隔路径从对象获取嵌套值
|
||||
*/
|
||||
function getNestedValue(obj: Translations, key: string): string | undefined {
|
||||
const keys = key.split('.');
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const k of keys) {
|
||||
if (current && typeof current === 'object' && k in current) {
|
||||
current = (current as Record<string, unknown>)[k];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate parameters into translation string
|
||||
* 将参数插入翻译字符串
|
||||
*/
|
||||
function interpolate(text: string, params?: TranslationParams): string {
|
||||
if (!params) return text;
|
||||
|
||||
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
||||
const value = params[key];
|
||||
return value !== undefined ? String(value) : `{{${key}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a locale hook for a plugin with its own translations
|
||||
* 为插件创建一个带有自己翻译的 locale hook
|
||||
*
|
||||
* This factory creates a React hook that:
|
||||
* 1. Syncs with the central LocaleService for locale changes
|
||||
* 2. Uses plugin-specific translations
|
||||
* 3. Falls back to English if translation not found
|
||||
*
|
||||
* 这个工厂创建一个 React hook,它会:
|
||||
* 1. 与中央 LocaleService 同步语言变化
|
||||
* 2. 使用插件特定的翻译
|
||||
* 3. 如果找不到翻译则回退到英语
|
||||
*
|
||||
* @param translations - Plugin translations bundle | 插件翻译包
|
||||
* @returns A React hook for accessing translations | 用于访问翻译的 React hook
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In your plugin's hooks folder:
|
||||
* // 在你的插件 hooks 文件夹中:
|
||||
* import { createPluginLocale } from '@esengine/editor-runtime';
|
||||
* import { en, zh, es } from '../locales';
|
||||
*
|
||||
* export const useTilemapLocale = createPluginLocale({ en, zh, es });
|
||||
*
|
||||
* // In your components:
|
||||
* // 在你的组件中:
|
||||
* const { t, locale } = useTilemapLocale();
|
||||
* return <button>{t('toolbar.save')}</button>;
|
||||
* ```
|
||||
*/
|
||||
export function createPluginLocale<T extends Translations>(
|
||||
translations: PluginTranslationsBundle<T>
|
||||
): () => PluginLocaleHook {
|
||||
const allTranslations = {
|
||||
en: translations.en,
|
||||
zh: translations.zh,
|
||||
es: translations.es || translations.en // Fallback to English if no Spanish
|
||||
};
|
||||
|
||||
return function usePluginLocale(): PluginLocaleHook {
|
||||
const [locale, setLocale] = useState<Locale>('en');
|
||||
|
||||
useEffect(() => {
|
||||
// Try to get LocaleService and sync with it
|
||||
// 尝试获取 LocaleService 并与之同步
|
||||
try {
|
||||
const localeService = Core.services.tryResolve(LocaleService);
|
||||
if (localeService) {
|
||||
setLocale(localeService.getCurrentLocale());
|
||||
|
||||
// Subscribe to locale changes
|
||||
// 订阅语言变化
|
||||
return localeService.onChange((newLocale) => {
|
||||
setLocale(newLocale);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// LocaleService not available, use default
|
||||
// LocaleService 不可用,使用默认值
|
||||
}
|
||||
}, []);
|
||||
|
||||
const t = useCallback((key: string, params?: TranslationParams): string => {
|
||||
const currentTranslations = allTranslations[locale] || allTranslations.en;
|
||||
const value = getNestedValue(currentTranslations as Translations, key);
|
||||
|
||||
if (value) {
|
||||
return interpolate(value, params);
|
||||
}
|
||||
|
||||
// Fallback to English if current locale doesn't have the key
|
||||
// 如果当前语言没有该键,回退到英语
|
||||
if (locale !== 'en') {
|
||||
const enValue = getNestedValue(allTranslations.en as Translations, key);
|
||||
if (enValue) {
|
||||
return interpolate(enValue, params);
|
||||
}
|
||||
}
|
||||
|
||||
// Return key itself as last resort
|
||||
// 最后返回键本身
|
||||
return key;
|
||||
}, [locale]);
|
||||
|
||||
return { t, locale };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a non-React translation function for a plugin
|
||||
* 为插件创建一个非 React 翻译函数
|
||||
*
|
||||
* Use this for translating in non-React contexts (services, utilities, etc.)
|
||||
* 在非 React 上下文(服务、工具类等)中使用此函数进行翻译
|
||||
*
|
||||
* @param translations - Plugin translations bundle | 插件翻译包
|
||||
* @returns A translation function | 翻译函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Create translator
|
||||
* // 创建翻译器
|
||||
* const translate = createPluginTranslator({ en, zh, es });
|
||||
*
|
||||
* // Use in non-React code
|
||||
* // 在非 React 代码中使用
|
||||
* const message = translate('errors.notFound', 'en');
|
||||
* ```
|
||||
*/
|
||||
export function createPluginTranslator<T extends Translations>(
|
||||
translations: PluginTranslationsBundle<T>
|
||||
): (key: string, locale?: Locale, params?: TranslationParams) => string {
|
||||
const allTranslations = {
|
||||
en: translations.en,
|
||||
zh: translations.zh,
|
||||
es: translations.es || translations.en
|
||||
};
|
||||
|
||||
return function translate(
|
||||
key: string,
|
||||
locale: Locale = 'en',
|
||||
params?: TranslationParams
|
||||
): string {
|
||||
const currentTranslations = allTranslations[locale] || allTranslations.en;
|
||||
const value = getNestedValue(currentTranslations as Translations, key);
|
||||
|
||||
if (value) {
|
||||
return interpolate(value, params);
|
||||
}
|
||||
|
||||
if (locale !== 'en') {
|
||||
const enValue = getNestedValue(allTranslations.en as Translations, key);
|
||||
if (enValue) {
|
||||
return interpolate(enValue, params);
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current locale from LocaleService
|
||||
* 从 LocaleService 获取当前语言
|
||||
*
|
||||
* Use this in non-React contexts where you need the current locale
|
||||
* 在需要当前语言的非 React 上下文中使用
|
||||
*
|
||||
* @returns Current locale or 'en' as default | 当前语言或默认 'en'
|
||||
*/
|
||||
export function getCurrentLocale(): Locale {
|
||||
try {
|
||||
const localeService = Core.services.tryResolve(LocaleService);
|
||||
if (localeService) {
|
||||
return localeService.getCurrentLocale();
|
||||
}
|
||||
} catch {
|
||||
// LocaleService not available
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
19
packages/editor-runtime/src/i18n/index.ts
Normal file
19
packages/editor-runtime/src/i18n/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Plugin i18n Infrastructure
|
||||
* 插件国际化基础设施
|
||||
*
|
||||
* Exports utilities for plugins to create their own locale systems
|
||||
* that integrate with the central LocaleService.
|
||||
*
|
||||
* 导出供插件创建自己的本地化系统的工具,
|
||||
* 这些系统会与中央 LocaleService 集成。
|
||||
*/
|
||||
|
||||
export {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale,
|
||||
type Translations,
|
||||
type PluginTranslationsBundle,
|
||||
type PluginLocaleHook
|
||||
} from './createPluginLocale';
|
||||
@@ -328,6 +328,18 @@ export type {
|
||||
UIScrollViewConfig,
|
||||
} from '@esengine/ui';
|
||||
|
||||
// =============================================================================
|
||||
// Plugin i18n Infrastructure
|
||||
// =============================================================================
|
||||
export {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale,
|
||||
type Translations as PluginTranslations,
|
||||
type PluginTranslationsBundle,
|
||||
type PluginLocaleHook
|
||||
} from './i18n';
|
||||
|
||||
// =============================================================================
|
||||
// SDK Metadata
|
||||
// =============================================================================
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Core } from '@esengine/ecs-framework';
|
||||
import { MessageHub, IFileSystemService } from '@esengine/editor-core';
|
||||
import { BlendMode, BuiltInShaders } from '@esengine/material-system';
|
||||
import { useMaterialEditorStore, createDefaultMaterialData } from '../stores/MaterialEditorStore';
|
||||
import { useMaterialLocale } from '../hooks/useMaterialLocale';
|
||||
import { Save, RefreshCw, FolderOpen } from 'lucide-react';
|
||||
import '../styles/MaterialEditorPanel.css';
|
||||
|
||||
@@ -19,25 +20,27 @@ type IFileSystem = {
|
||||
|
||||
/**
|
||||
* 混合模式选项
|
||||
* Blend mode options with translation keys
|
||||
*/
|
||||
const BLEND_MODE_OPTIONS = [
|
||||
{ value: BlendMode.None, label: 'None (Opaque)', labelZh: '无 (不透明)' },
|
||||
{ value: BlendMode.Alpha, label: 'Alpha Blend', labelZh: 'Alpha 混合' },
|
||||
{ value: BlendMode.Additive, label: 'Additive', labelZh: '叠加' },
|
||||
{ value: BlendMode.Multiply, label: 'Multiply', labelZh: '正片叠底' },
|
||||
{ value: BlendMode.Screen, label: 'Screen', labelZh: '滤色' },
|
||||
{ value: BlendMode.PremultipliedAlpha, label: 'Premultiplied Alpha', labelZh: '预乘 Alpha' },
|
||||
{ value: BlendMode.None, labelKey: 'blendModes.none' },
|
||||
{ value: BlendMode.Alpha, labelKey: 'blendModes.alpha' },
|
||||
{ value: BlendMode.Additive, labelKey: 'blendModes.additive' },
|
||||
{ value: BlendMode.Multiply, labelKey: 'blendModes.multiply' },
|
||||
{ value: BlendMode.Screen, labelKey: 'blendModes.screen' },
|
||||
{ value: BlendMode.PremultipliedAlpha, labelKey: 'blendModes.premultipliedAlpha' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 内置着色器选项
|
||||
* Built-in shader options with translation keys
|
||||
*/
|
||||
const BUILT_IN_SHADER_OPTIONS = [
|
||||
{ value: BuiltInShaders.DefaultSprite, label: 'Default Sprite', labelZh: '默认精灵' },
|
||||
{ value: BuiltInShaders.Grayscale, label: 'Grayscale', labelZh: '灰度' },
|
||||
{ value: BuiltInShaders.Tint, label: 'Tint', labelZh: '着色' },
|
||||
{ value: BuiltInShaders.Flash, label: 'Flash', labelZh: '闪烁' },
|
||||
{ value: BuiltInShaders.Outline, label: 'Outline', labelZh: '描边' },
|
||||
{ value: BuiltInShaders.DefaultSprite, labelKey: 'shaders.defaultSprite' },
|
||||
{ value: BuiltInShaders.Grayscale, labelKey: 'shaders.grayscale' },
|
||||
{ value: BuiltInShaders.Tint, labelKey: 'shaders.tint' },
|
||||
{ value: BuiltInShaders.Flash, labelKey: 'shaders.flash' },
|
||||
{ value: BuiltInShaders.Outline, labelKey: 'shaders.outline' },
|
||||
];
|
||||
|
||||
/** Custom shader indicator value. | 自定义着色器指示值。 */
|
||||
@@ -47,7 +50,8 @@ interface MaterialEditorPanelProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps) {
|
||||
export function MaterialEditorPanel({ locale: _locale }: MaterialEditorPanelProps) {
|
||||
const { t } = useMaterialLocale();
|
||||
const {
|
||||
currentFilePath,
|
||||
pendingFilePath,
|
||||
@@ -61,8 +65,6 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
updateMaterialProperty,
|
||||
} = useMaterialEditorStore();
|
||||
|
||||
const isZh = locale === 'zh';
|
||||
|
||||
// 加载材质文件
|
||||
const loadMaterialFile = useCallback(async (filePath: string) => {
|
||||
setLoading(true);
|
||||
@@ -136,7 +138,7 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
return (
|
||||
<div className="material-editor-panel loading">
|
||||
<RefreshCw className="spin" size={24} />
|
||||
<span>{isZh ? '加载中...' : 'Loading...'}</span>
|
||||
<span>{t('panel.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -145,7 +147,7 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
if (!materialData) {
|
||||
return (
|
||||
<div className="material-editor-panel empty">
|
||||
<span>{isZh ? '双击 .mat 文件打开材质编辑器' : 'Double-click a .mat file to open the material editor'}</span>
|
||||
<span>{t('panel.emptyState')}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -163,10 +165,10 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
className="toolbar-button"
|
||||
onClick={saveMaterialFile}
|
||||
disabled={!isDirty}
|
||||
title={isZh ? '保存 (Ctrl+S)' : 'Save (Ctrl+S)'}
|
||||
title={t('panel.saveTooltip')}
|
||||
>
|
||||
<Save size={16} />
|
||||
<span>{isZh ? '保存' : 'Save'}</span>
|
||||
<span>{t('panel.save')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,10 +177,10 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
<div className="material-editor-content">
|
||||
{/* 基本属性 */}
|
||||
<div className="property-section">
|
||||
<div className="section-header">{isZh ? '基本属性' : 'Basic Properties'}</div>
|
||||
<div className="section-header">{t('properties.basicTitle')}</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>{isZh ? '名称' : 'Name'}</label>
|
||||
<label>{t('properties.name')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={materialData.name}
|
||||
@@ -187,7 +189,7 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
</div>
|
||||
|
||||
<div className="property-row">
|
||||
<label>{isZh ? '着色器' : 'Shader'}</label>
|
||||
<label>{t('properties.shader')}</label>
|
||||
<div className="shader-selector">
|
||||
<select
|
||||
value={typeof materialData.shader === 'string' ? CUSTOM_SHADER_VALUE : materialData.shader}
|
||||
@@ -205,11 +207,11 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
>
|
||||
{BUILT_IN_SHADER_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{isZh ? opt.labelZh : opt.label}
|
||||
{t(opt.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
<option value={CUSTOM_SHADER_VALUE}>
|
||||
{isZh ? '自定义着色器...' : 'Custom Shader...'}
|
||||
{t('properties.customShader')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -218,13 +220,13 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
{/* Custom shader path input */}
|
||||
{typeof materialData.shader === 'string' && (
|
||||
<div className="property-row">
|
||||
<label>{isZh ? '着色器路径' : 'Shader Path'}</label>
|
||||
<label>{t('properties.shaderPath')}</label>
|
||||
<div className="file-input-row">
|
||||
<input
|
||||
type="text"
|
||||
value={materialData.shader}
|
||||
onChange={(e) => updateMaterialProperty('shader', e.target.value)}
|
||||
placeholder={isZh ? '输入 .shader 文件路径' : 'Enter .shader file path'}
|
||||
placeholder={t('properties.shaderPathPlaceholder')}
|
||||
/>
|
||||
<button
|
||||
className="browse-button"
|
||||
@@ -232,7 +234,7 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
// TODO: Implement file browser dialog
|
||||
// 这里可以集成编辑器的文件选择对话框
|
||||
}}
|
||||
title={isZh ? '浏览...' : 'Browse...'}
|
||||
title={t('properties.browse')}
|
||||
>
|
||||
<FolderOpen size={14} />
|
||||
</button>
|
||||
@@ -241,14 +243,14 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
)}
|
||||
|
||||
<div className="property-row">
|
||||
<label>{isZh ? '混合模式' : 'Blend Mode'}</label>
|
||||
<label>{t('properties.blendMode')}</label>
|
||||
<select
|
||||
value={materialData.blendMode}
|
||||
onChange={(e) => updateMaterialProperty('blendMode', Number(e.target.value))}
|
||||
>
|
||||
{BLEND_MODE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{isZh ? opt.labelZh : opt.label}
|
||||
{t(opt.labelKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -257,11 +259,11 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
|
||||
{/* Uniform 参数 */}
|
||||
<div className="property-section">
|
||||
<div className="section-header">{isZh ? 'Uniform 参数' : 'Uniform Parameters'}</div>
|
||||
<div className="section-header">{t('uniforms.title')}</div>
|
||||
|
||||
{Object.keys(materialData.uniforms || {}).length === 0 ? (
|
||||
<div className="empty-uniforms">
|
||||
{isZh ? '该着色器没有自定义参数' : 'This shader has no custom parameters'}
|
||||
{t('uniforms.empty')}
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(materialData.uniforms || {}).map(([key, uniform]) => (
|
||||
@@ -275,9 +277,9 @@ export function MaterialEditorPanel({ locale = 'en' }: MaterialEditorPanelProps)
|
||||
|
||||
{/* 文件信息 */}
|
||||
<div className="property-section">
|
||||
<div className="section-header">{isZh ? '文件信息' : 'File Info'}</div>
|
||||
<div className="section-header">{t('fileInfo.title')}</div>
|
||||
<div className="property-row file-path">
|
||||
<label>{isZh ? '路径' : 'Path'}</label>
|
||||
<label>{t('fileInfo.path')}</label>
|
||||
<span title={currentFilePath || ''}>
|
||||
{currentFilePath?.split(/[\\/]/).pop() || '-'}
|
||||
</span>
|
||||
|
||||
5
packages/material-editor/src/hooks/index.ts
Normal file
5
packages/material-editor/src/hooks/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Material Editor Hooks
|
||||
* 材质编辑器钩子导出
|
||||
*/
|
||||
export { useMaterialLocale, translateMaterial } from './useMaterialLocale';
|
||||
169
packages/material-editor/src/hooks/useMaterialLocale.ts
Normal file
169
packages/material-editor/src/hooks/useMaterialLocale.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* Material Editor Locale Hook
|
||||
* 材质编辑器语言钩子
|
||||
*
|
||||
* 提供材质编辑器专用的翻译功能
|
||||
*/
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { LocaleService } from '@esengine/editor-core';
|
||||
import { en, zh, es } from '../locales';
|
||||
|
||||
type Locale = 'en' | 'zh' | 'es';
|
||||
type TranslationParams = Record<string, string | number>;
|
||||
|
||||
const translations = { en, zh, es } as const;
|
||||
|
||||
/**
|
||||
* 获取嵌套对象的值
|
||||
* Get nested object value by dot notation key
|
||||
*/
|
||||
function getNestedValue(obj: Record<string, unknown>, key: string): string | undefined {
|
||||
const keys = key.split('.');
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const k of keys) {
|
||||
if (current && typeof current === 'object' && k in current) {
|
||||
current = (current as Record<string, unknown>)[k];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return typeof current === 'string' ? current : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换参数占位符
|
||||
* Replace parameter placeholders in string
|
||||
*/
|
||||
function interpolate(text: string, params?: TranslationParams): string {
|
||||
if (!params) return text;
|
||||
|
||||
return text.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
||||
const value = params[key];
|
||||
return value !== undefined ? String(value) : `{{${key}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试从 LocaleService 获取当前语言
|
||||
* Try to get current locale from LocaleService
|
||||
*/
|
||||
function tryGetLocaleFromService(): Locale | null {
|
||||
try {
|
||||
// 尝试动态获取 LocaleService
|
||||
const localeService = Core.services.tryResolve(LocaleService);
|
||||
|
||||
if (localeService?.getCurrentLocale) {
|
||||
return localeService.getCurrentLocale() as Locale;
|
||||
}
|
||||
} catch {
|
||||
// LocaleService 不可用
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅语言变化
|
||||
* Subscribe to locale changes
|
||||
*/
|
||||
function subscribeToLocaleChanges(callback: (locale: Locale) => void): (() => void) | undefined {
|
||||
try {
|
||||
const localeService = Core.services.tryResolve(LocaleService);
|
||||
|
||||
if (localeService?.onChange) {
|
||||
return localeService.onChange((newLocale) => {
|
||||
callback(newLocale as Locale);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// LocaleService 不可用
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for accessing material editor translations
|
||||
* 访问材质编辑器翻译的 Hook
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { t, locale } = useMaterialLocale();
|
||||
* return <button title={t('panel.saveTooltip')}>{t('panel.save')}</button>;
|
||||
* ```
|
||||
*/
|
||||
export function useMaterialLocale() {
|
||||
const [locale, setLocale] = useState<Locale>(() => {
|
||||
return tryGetLocaleFromService() || 'en';
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// 初始化时获取当前语言
|
||||
const currentLocale = tryGetLocaleFromService();
|
||||
if (currentLocale) {
|
||||
setLocale(currentLocale);
|
||||
}
|
||||
|
||||
// 订阅语言变化
|
||||
const unsubscribe = subscribeToLocaleChanges((newLocale) => {
|
||||
setLocale(newLocale);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
* Translation function
|
||||
*
|
||||
* @param key - 翻译键,如 'panel.save'
|
||||
* @param params - 插值参数
|
||||
* @param fallback - 回退文本
|
||||
*/
|
||||
const t = useCallback((key: string, params?: TranslationParams, fallback?: string): string => {
|
||||
const currentTranslations = translations[locale] || translations.en;
|
||||
const value = getNestedValue(currentTranslations as Record<string, unknown>, key);
|
||||
|
||||
if (value) {
|
||||
return interpolate(value, params);
|
||||
}
|
||||
|
||||
// 如果当前语言没有,尝试英文
|
||||
if (locale !== 'en') {
|
||||
const enValue = getNestedValue(translations.en as Record<string, unknown>, key);
|
||||
if (enValue) {
|
||||
return interpolate(enValue, params);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回 fallback 或 key 本身
|
||||
return fallback || key;
|
||||
}, [locale]);
|
||||
|
||||
return { t, locale, setLocale };
|
||||
}
|
||||
|
||||
/**
|
||||
* 非 React 环境下的翻译函数
|
||||
* Translation function for non-React context
|
||||
*/
|
||||
export function translateMaterial(key: string, locale: Locale = 'en', params?: TranslationParams): string {
|
||||
const currentTranslations = translations[locale] || translations.en;
|
||||
const value = getNestedValue(currentTranslations as Record<string, unknown>, key);
|
||||
|
||||
if (value) {
|
||||
return interpolate(value, params);
|
||||
}
|
||||
|
||||
if (locale !== 'en') {
|
||||
const enValue = getNestedValue(translations.en as Record<string, unknown>, key);
|
||||
if (enValue) {
|
||||
return interpolate(enValue, params);
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
@@ -21,9 +21,13 @@ import {
|
||||
FileActionRegistry,
|
||||
InspectorRegistry,
|
||||
IInspectorRegistry,
|
||||
IFileSystemService
|
||||
IFileSystemService,
|
||||
LocaleService
|
||||
} from '@esengine/editor-core';
|
||||
|
||||
// Import locale translations
|
||||
import { en, zh, es } from './locales';
|
||||
|
||||
// Inspector provider
|
||||
import { MaterialAssetInspectorProvider } from './providers/MaterialAssetInspectorProvider';
|
||||
|
||||
@@ -103,6 +107,25 @@ export class MaterialEditorModule implements IEditorModuleLoader {
|
||||
await this.handleCreateMaterialAsset(payload);
|
||||
});
|
||||
}
|
||||
|
||||
// Register translations
|
||||
this.registerTranslations(services);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件翻译到 LocaleService
|
||||
* Register plugin translations to LocaleService
|
||||
*/
|
||||
private registerTranslations(services: ServiceContainer): void {
|
||||
try {
|
||||
const localeService = services.tryResolve(LocaleService);
|
||||
if (localeService) {
|
||||
localeService.extendTranslations('material', { en, zh, es });
|
||||
console.info('[MaterialEditorModule] Translations registered');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MaterialEditorModule] Failed to register translations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
@@ -234,6 +257,7 @@ export const materialEditorModule = new MaterialEditorModule();
|
||||
export { MaterialEditorPanel } from './components/MaterialEditorPanel';
|
||||
export { useMaterialEditorStore, createDefaultMaterialData } from './stores/MaterialEditorStore';
|
||||
export type { MaterialEditorState } from './stores/MaterialEditorStore';
|
||||
export { useMaterialLocale, translateMaterial } from './hooks/useMaterialLocale';
|
||||
|
||||
/**
|
||||
* Material Plugin Manifest
|
||||
|
||||
71
packages/material-editor/src/locales/en.ts
Normal file
71
packages/material-editor/src/locales/en.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Material Editor English Translations
|
||||
* 材质编辑器英文翻译
|
||||
*/
|
||||
export const en = {
|
||||
// Editor Panel
|
||||
panel: {
|
||||
loading: 'Loading...',
|
||||
emptyState: 'Double-click a .mat file to open the material editor',
|
||||
save: 'Save',
|
||||
saveTooltip: 'Save (Ctrl+S)',
|
||||
},
|
||||
|
||||
// Properties Section
|
||||
properties: {
|
||||
basicTitle: 'Basic Properties',
|
||||
name: 'Name',
|
||||
shader: 'Shader',
|
||||
shaderPath: 'Shader Path',
|
||||
shaderPathPlaceholder: 'Enter .shader file path',
|
||||
browse: 'Browse...',
|
||||
blendMode: 'Blend Mode',
|
||||
customShader: 'Custom Shader...',
|
||||
},
|
||||
|
||||
// Uniforms Section
|
||||
uniforms: {
|
||||
title: 'Uniform Parameters',
|
||||
empty: 'This shader has no custom parameters',
|
||||
namePlaceholder: 'Uniform name...',
|
||||
addTooltip: 'Add uniform',
|
||||
removeTooltip: 'Remove uniform',
|
||||
},
|
||||
|
||||
// File Info Section
|
||||
fileInfo: {
|
||||
title: 'File Info',
|
||||
path: 'Path',
|
||||
},
|
||||
|
||||
// Blend Mode Options
|
||||
blendModes: {
|
||||
none: 'None (Opaque)',
|
||||
alpha: 'Alpha Blend',
|
||||
additive: 'Additive',
|
||||
multiply: 'Multiply',
|
||||
screen: 'Screen',
|
||||
premultipliedAlpha: 'Premultiplied Alpha',
|
||||
},
|
||||
|
||||
// Built-in Shader Options
|
||||
shaders: {
|
||||
defaultSprite: 'Default Sprite',
|
||||
grayscale: 'Grayscale',
|
||||
tint: 'Tint',
|
||||
flash: 'Flash',
|
||||
outline: 'Outline',
|
||||
},
|
||||
|
||||
// Inspector Panel
|
||||
inspector: {
|
||||
saveTooltip: 'Save (Ctrl+S)',
|
||||
save: 'Save',
|
||||
reset: 'Reset',
|
||||
resetTooltip: 'Reset changes',
|
||||
basicProperties: 'Basic Properties',
|
||||
uniforms: 'Uniforms',
|
||||
loading: 'Loading...',
|
||||
parseError: 'Failed to parse material file',
|
||||
},
|
||||
};
|
||||
71
packages/material-editor/src/locales/es.ts
Normal file
71
packages/material-editor/src/locales/es.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Material Editor Spanish Translations
|
||||
* 材质编辑器西班牙语翻译
|
||||
*/
|
||||
export const es = {
|
||||
// Editor Panel
|
||||
panel: {
|
||||
loading: 'Cargando...',
|
||||
emptyState: 'Haga doble clic en un archivo .mat para abrir el editor de materiales',
|
||||
save: 'Guardar',
|
||||
saveTooltip: 'Guardar (Ctrl+S)',
|
||||
},
|
||||
|
||||
// Properties Section
|
||||
properties: {
|
||||
basicTitle: 'Propiedades Básicas',
|
||||
name: 'Nombre',
|
||||
shader: 'Shader',
|
||||
shaderPath: 'Ruta del Shader',
|
||||
shaderPathPlaceholder: 'Ingrese la ruta del archivo .shader',
|
||||
browse: 'Explorar...',
|
||||
blendMode: 'Modo de Mezcla',
|
||||
customShader: 'Shader Personalizado...',
|
||||
},
|
||||
|
||||
// Uniforms Section
|
||||
uniforms: {
|
||||
title: 'Parámetros Uniform',
|
||||
empty: 'Este shader no tiene parámetros personalizados',
|
||||
namePlaceholder: 'Nombre del uniform...',
|
||||
addTooltip: 'Agregar uniform',
|
||||
removeTooltip: 'Eliminar uniform',
|
||||
},
|
||||
|
||||
// File Info Section
|
||||
fileInfo: {
|
||||
title: 'Información del Archivo',
|
||||
path: 'Ruta',
|
||||
},
|
||||
|
||||
// Blend Mode Options
|
||||
blendModes: {
|
||||
none: 'Ninguno (Opaco)',
|
||||
alpha: 'Mezcla Alpha',
|
||||
additive: 'Aditivo',
|
||||
multiply: 'Multiplicar',
|
||||
screen: 'Pantalla',
|
||||
premultipliedAlpha: 'Alpha Premultiplicado',
|
||||
},
|
||||
|
||||
// Built-in Shader Options
|
||||
shaders: {
|
||||
defaultSprite: 'Sprite por Defecto',
|
||||
grayscale: 'Escala de Grises',
|
||||
tint: 'Tinte',
|
||||
flash: 'Destello',
|
||||
outline: 'Contorno',
|
||||
},
|
||||
|
||||
// Inspector Panel
|
||||
inspector: {
|
||||
saveTooltip: 'Guardar (Ctrl+S)',
|
||||
save: 'Guardar',
|
||||
reset: 'Restablecer',
|
||||
resetTooltip: 'Restablecer cambios',
|
||||
basicProperties: 'Propiedades Básicas',
|
||||
uniforms: 'Uniforms',
|
||||
loading: 'Cargando...',
|
||||
parseError: 'Error al analizar el archivo de material',
|
||||
},
|
||||
};
|
||||
7
packages/material-editor/src/locales/index.ts
Normal file
7
packages/material-editor/src/locales/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Material Editor Locale Exports
|
||||
* 材质编辑器语言导出
|
||||
*/
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
71
packages/material-editor/src/locales/zh.ts
Normal file
71
packages/material-editor/src/locales/zh.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Material Editor Chinese Translations
|
||||
* 材质编辑器中文翻译
|
||||
*/
|
||||
export const zh = {
|
||||
// Editor Panel
|
||||
panel: {
|
||||
loading: '加载中...',
|
||||
emptyState: '双击 .mat 文件打开材质编辑器',
|
||||
save: '保存',
|
||||
saveTooltip: '保存 (Ctrl+S)',
|
||||
},
|
||||
|
||||
// Properties Section
|
||||
properties: {
|
||||
basicTitle: '基本属性',
|
||||
name: '名称',
|
||||
shader: '着色器',
|
||||
shaderPath: '着色器路径',
|
||||
shaderPathPlaceholder: '输入 .shader 文件路径',
|
||||
browse: '浏览...',
|
||||
blendMode: '混合模式',
|
||||
customShader: '自定义着色器...',
|
||||
},
|
||||
|
||||
// Uniforms Section
|
||||
uniforms: {
|
||||
title: 'Uniform 参数',
|
||||
empty: '该着色器没有自定义参数',
|
||||
namePlaceholder: 'Uniform 名称...',
|
||||
addTooltip: '添加 uniform',
|
||||
removeTooltip: '删除 uniform',
|
||||
},
|
||||
|
||||
// File Info Section
|
||||
fileInfo: {
|
||||
title: '文件信息',
|
||||
path: '路径',
|
||||
},
|
||||
|
||||
// Blend Mode Options
|
||||
blendModes: {
|
||||
none: '无 (不透明)',
|
||||
alpha: 'Alpha 混合',
|
||||
additive: '叠加',
|
||||
multiply: '正片叠底',
|
||||
screen: '滤色',
|
||||
premultipliedAlpha: '预乘 Alpha',
|
||||
},
|
||||
|
||||
// Built-in Shader Options
|
||||
shaders: {
|
||||
defaultSprite: '默认精灵',
|
||||
grayscale: '灰度',
|
||||
tint: '着色',
|
||||
flash: '闪烁',
|
||||
outline: '描边',
|
||||
},
|
||||
|
||||
// Inspector Panel
|
||||
inspector: {
|
||||
saveTooltip: '保存 (Ctrl+S)',
|
||||
save: '保存',
|
||||
reset: '重置',
|
||||
resetTooltip: '重置更改',
|
||||
basicProperties: '基本属性',
|
||||
uniforms: 'Uniforms',
|
||||
loading: '加载中...',
|
||||
parseError: '材质文件解析失败',
|
||||
},
|
||||
};
|
||||
@@ -31,6 +31,7 @@
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
70
packages/particle-editor/src/hooks/useParticleLocale.ts
Normal file
70
packages/particle-editor/src/hooks/useParticleLocale.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Particle Editor Locale Hook
|
||||
* 粒子编辑器语言钩子
|
||||
*
|
||||
* Uses the unified plugin i18n infrastructure from editor-runtime.
|
||||
* 使用 editor-runtime 的统一插件国际化基础设施。
|
||||
*/
|
||||
import {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale
|
||||
} from '@esengine/editor-runtime';
|
||||
import { en, zh, es } from '../locales';
|
||||
import type { Locale, TranslationParams } from '@esengine/editor-core';
|
||||
|
||||
// Create translations bundle
|
||||
// 创建翻译包
|
||||
const translations = { en, zh, es };
|
||||
|
||||
/**
|
||||
* Hook for accessing particle editor translations
|
||||
* 访问粒子编辑器翻译的 Hook
|
||||
*
|
||||
* Uses the unified createPluginLocale factory from editor-runtime.
|
||||
* 使用 editor-runtime 的统一 createPluginLocale 工厂。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { t, locale } = useParticleLocale();
|
||||
* return <PropertyInput label={t('basic.name')} />;
|
||||
* ```
|
||||
*/
|
||||
export const useParticleLocale = createPluginLocale(translations);
|
||||
|
||||
// Create non-React translator using the unified infrastructure
|
||||
// 使用统一基础设施创建非 React 翻译器
|
||||
const particleTranslator = createPluginTranslator(translations);
|
||||
|
||||
/**
|
||||
* Non-React translation function for particle editor
|
||||
* 粒子编辑器的非 React 翻译函数
|
||||
*
|
||||
* Use this in services, utilities, and other non-React contexts.
|
||||
* 在服务、工具类和其他非 React 上下文中使用。
|
||||
*
|
||||
* @param key - Translation key | 翻译键
|
||||
* @param locale - Optional locale, defaults to current locale | 可选语言,默认使用当前语言
|
||||
* @param params - Optional interpolation parameters | 可选插值参数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit locale
|
||||
* translateParticle('notifications.fileSaved', 'zh', { path: '/path/to/file' });
|
||||
*
|
||||
* // With current locale (auto-detected)
|
||||
* translateParticle('basic.name');
|
||||
* ```
|
||||
*/
|
||||
export function translateParticle(
|
||||
key: string,
|
||||
locale?: Locale,
|
||||
params?: TranslationParams
|
||||
): string {
|
||||
const targetLocale = locale || getCurrentLocale();
|
||||
return particleTranslator(key, targetLocale, params);
|
||||
}
|
||||
|
||||
// Re-export for external use
|
||||
// 重新导出供外部使用
|
||||
export { getCurrentLocale } from '@esengine/editor-runtime';
|
||||
267
packages/particle-editor/src/locales/en.ts
Normal file
267
packages/particle-editor/src/locales/en.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* English translations for Particle Editor
|
||||
* 粒子编辑器英文翻译
|
||||
*/
|
||||
export const en = {
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Particle Editor',
|
||||
noFileOpen: 'No particle file is open',
|
||||
dropToOpen: 'Drop a .particle file here or use Open button'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
play: 'Play',
|
||||
pause: 'Pause',
|
||||
restart: 'Restart',
|
||||
save: 'Save',
|
||||
open: 'Open',
|
||||
maximize: 'Maximize preview',
|
||||
minimize: 'Minimize preview',
|
||||
followMouse: 'Follow mouse',
|
||||
resetPosition: 'Reset position'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Sections
|
||||
// ========================================
|
||||
sections: {
|
||||
basic: 'Basic',
|
||||
emission: 'Emission',
|
||||
particle: 'Particle',
|
||||
color: 'Color',
|
||||
modules: 'Modules',
|
||||
presets: 'Presets'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Basic Properties
|
||||
// ========================================
|
||||
basic: {
|
||||
name: 'Name',
|
||||
texture: 'Texture',
|
||||
maxParticles: 'Max Particles',
|
||||
looping: 'Looping',
|
||||
duration: 'Duration',
|
||||
prewarm: 'Prewarm',
|
||||
playSpeed: 'Play Speed',
|
||||
blendMode: 'Blend Mode',
|
||||
space: 'Space',
|
||||
particleSize: 'Particle Size',
|
||||
sortOrder: 'Sort Order'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blend Modes
|
||||
// ========================================
|
||||
blendMode: {
|
||||
normal: 'Normal',
|
||||
additive: 'Additive',
|
||||
multiply: 'Multiply'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Simulation Space
|
||||
// ========================================
|
||||
space: {
|
||||
world: 'World',
|
||||
local: 'Local'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Emission Properties
|
||||
// ========================================
|
||||
emission: {
|
||||
rate: 'Rate',
|
||||
shape: 'Shape',
|
||||
radius: 'Radius',
|
||||
width: 'Width',
|
||||
height: 'Height',
|
||||
coneAngle: 'Cone Angle'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Emission Shapes
|
||||
// ========================================
|
||||
shapes: {
|
||||
point: 'Point',
|
||||
circle: 'Circle',
|
||||
ring: 'Ring',
|
||||
rectangle: 'Rectangle',
|
||||
edge: 'Edge',
|
||||
line: 'Line',
|
||||
cone: 'Cone'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Particle Properties
|
||||
// ========================================
|
||||
particle: {
|
||||
lifetime: 'Lifetime',
|
||||
speed: 'Speed',
|
||||
direction: 'Direction',
|
||||
spread: 'Spread',
|
||||
scale: 'Scale',
|
||||
gravity: 'Gravity'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Color Properties
|
||||
// ========================================
|
||||
color: {
|
||||
startColor: 'Start Color',
|
||||
startAlpha: 'Start Alpha',
|
||||
endAlpha: 'End Alpha',
|
||||
endScale: 'End Scale'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Module Names
|
||||
// ========================================
|
||||
modules: {
|
||||
colorOverLifetime: 'Color Over Lifetime',
|
||||
sizeOverLifetime: 'Size Over Lifetime',
|
||||
velocityOverLifetime: 'Velocity Over Lifetime',
|
||||
rotationOverLifetime: 'Rotation Over Lifetime',
|
||||
noise: 'Noise',
|
||||
collision: 'Collision',
|
||||
forceField: 'Force Field'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Velocity Over Lifetime
|
||||
// ========================================
|
||||
velocity: {
|
||||
drag: 'Drag',
|
||||
orbital: 'Orbital',
|
||||
radial: 'Radial'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Rotation Over Lifetime
|
||||
// ========================================
|
||||
rotation: {
|
||||
startMult: 'Start Mult',
|
||||
endMult: 'End Mult',
|
||||
additional: 'Additional'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Noise Module
|
||||
// ========================================
|
||||
noise: {
|
||||
position: 'Position',
|
||||
velocity: 'Velocity',
|
||||
rotation: 'Rotation',
|
||||
frequency: 'Frequency',
|
||||
scroll: 'Scroll'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Module
|
||||
// ========================================
|
||||
collision: {
|
||||
boundary: 'Boundary',
|
||||
behavior: 'Behavior',
|
||||
left: 'Left',
|
||||
right: 'Right',
|
||||
top: 'Top',
|
||||
bottom: 'Bottom',
|
||||
radius: 'Radius',
|
||||
bounce: 'Bounce',
|
||||
lifeLoss: 'Life Loss'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Boundary Types
|
||||
// ========================================
|
||||
boundaryType: {
|
||||
none: 'None',
|
||||
rectangle: 'Rectangle',
|
||||
circle: 'Circle'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Behaviors
|
||||
// ========================================
|
||||
collisionBehavior: {
|
||||
kill: 'Kill',
|
||||
bounce: 'Bounce',
|
||||
wrap: 'Wrap'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Force Field Module
|
||||
// ========================================
|
||||
forceField: {
|
||||
type: 'Type',
|
||||
strength: 'Strength',
|
||||
directionX: 'Direction X',
|
||||
directionY: 'Direction Y',
|
||||
centerX: 'Center X',
|
||||
centerY: 'Center Y',
|
||||
range: 'Range',
|
||||
falloff: 'Falloff'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Force Field Types
|
||||
// ========================================
|
||||
forceFieldType: {
|
||||
wind: 'Wind',
|
||||
point: 'Point',
|
||||
vortex: 'Vortex',
|
||||
turbulence: 'Turbulence'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Curve Editor
|
||||
// ========================================
|
||||
curve: {
|
||||
deletePoint: 'Delete point',
|
||||
constant: 'Constant value',
|
||||
fadeIn: 'Fade in',
|
||||
fadeOut: 'Fade out',
|
||||
bellCurve: 'Bell curve',
|
||||
uCurve: 'U curve'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Gradient Editor
|
||||
// ========================================
|
||||
gradient: {
|
||||
deleteStop: 'Delete stop'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Texture Picker
|
||||
// ========================================
|
||||
texturePicker: {
|
||||
browse: 'Browse...',
|
||||
clear: 'Clear'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: 'File saved: {{path}}',
|
||||
fileSaveFailed: 'Failed to save file',
|
||||
fileOpened: 'File opened: {{path}}',
|
||||
fileOpenFailed: 'Failed to open file'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
selectTexture: 'Select texture image',
|
||||
selectParticleFile: 'Select particle file',
|
||||
saveParticleFile: 'Save particle file'
|
||||
}
|
||||
};
|
||||
267
packages/particle-editor/src/locales/es.ts
Normal file
267
packages/particle-editor/src/locales/es.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Spanish translations for Particle Editor
|
||||
* Traducciones en español del editor de partículas
|
||||
*/
|
||||
export const es = {
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Editor de Partículas',
|
||||
noFileOpen: 'No hay archivo de partículas abierto',
|
||||
dropToOpen: 'Arrastre un archivo .particle aquí o use el botón Abrir'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
play: 'Reproducir',
|
||||
pause: 'Pausar',
|
||||
restart: 'Reiniciar',
|
||||
save: 'Guardar',
|
||||
open: 'Abrir',
|
||||
maximize: 'Maximizar vista previa',
|
||||
minimize: 'Minimizar vista previa',
|
||||
followMouse: 'Seguir ratón',
|
||||
resetPosition: 'Restablecer posición'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Sections
|
||||
// ========================================
|
||||
sections: {
|
||||
basic: 'Básico',
|
||||
emission: 'Emisión',
|
||||
particle: 'Partícula',
|
||||
color: 'Color',
|
||||
modules: 'Módulos',
|
||||
presets: 'Preajustes'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Basic Properties
|
||||
// ========================================
|
||||
basic: {
|
||||
name: 'Nombre',
|
||||
texture: 'Textura',
|
||||
maxParticles: 'Máx. Partículas',
|
||||
looping: 'Bucle',
|
||||
duration: 'Duración',
|
||||
prewarm: 'Precalentamiento',
|
||||
playSpeed: 'Velocidad',
|
||||
blendMode: 'Modo mezcla',
|
||||
space: 'Espacio',
|
||||
particleSize: 'Tamaño partícula',
|
||||
sortOrder: 'Orden'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blend Modes
|
||||
// ========================================
|
||||
blendMode: {
|
||||
normal: 'Normal',
|
||||
additive: 'Aditivo',
|
||||
multiply: 'Multiplicar'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Simulation Space
|
||||
// ========================================
|
||||
space: {
|
||||
world: 'Mundo',
|
||||
local: 'Local'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Emission Properties
|
||||
// ========================================
|
||||
emission: {
|
||||
rate: 'Tasa',
|
||||
shape: 'Forma',
|
||||
radius: 'Radio',
|
||||
width: 'Ancho',
|
||||
height: 'Alto',
|
||||
coneAngle: 'Ángulo cono'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Emission Shapes
|
||||
// ========================================
|
||||
shapes: {
|
||||
point: 'Punto',
|
||||
circle: 'Círculo',
|
||||
ring: 'Anillo',
|
||||
rectangle: 'Rectángulo',
|
||||
edge: 'Borde',
|
||||
line: 'Línea',
|
||||
cone: 'Cono'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Particle Properties
|
||||
// ========================================
|
||||
particle: {
|
||||
lifetime: 'Vida',
|
||||
speed: 'Velocidad',
|
||||
direction: 'Dirección',
|
||||
spread: 'Dispersión',
|
||||
scale: 'Escala',
|
||||
gravity: 'Gravedad'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Color Properties
|
||||
// ========================================
|
||||
color: {
|
||||
startColor: 'Color inicial',
|
||||
startAlpha: 'Alfa inicial',
|
||||
endAlpha: 'Alfa final',
|
||||
endScale: 'Escala final'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Module Names
|
||||
// ========================================
|
||||
modules: {
|
||||
colorOverLifetime: 'Color durante vida',
|
||||
sizeOverLifetime: 'Tamaño durante vida',
|
||||
velocityOverLifetime: 'Velocidad durante vida',
|
||||
rotationOverLifetime: 'Rotación durante vida',
|
||||
noise: 'Ruido',
|
||||
collision: 'Colisión',
|
||||
forceField: 'Campo de fuerza'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Velocity Over Lifetime
|
||||
// ========================================
|
||||
velocity: {
|
||||
drag: 'Arrastre',
|
||||
orbital: 'Orbital',
|
||||
radial: 'Radial'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Rotation Over Lifetime
|
||||
// ========================================
|
||||
rotation: {
|
||||
startMult: 'Mult. inicial',
|
||||
endMult: 'Mult. final',
|
||||
additional: 'Adicional'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Noise Module
|
||||
// ========================================
|
||||
noise: {
|
||||
position: 'Posición',
|
||||
velocity: 'Velocidad',
|
||||
rotation: 'Rotación',
|
||||
frequency: 'Frecuencia',
|
||||
scroll: 'Desplazamiento'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Module
|
||||
// ========================================
|
||||
collision: {
|
||||
boundary: 'Límite',
|
||||
behavior: 'Comportamiento',
|
||||
left: 'Izquierda',
|
||||
right: 'Derecha',
|
||||
top: 'Arriba',
|
||||
bottom: 'Abajo',
|
||||
radius: 'Radio',
|
||||
bounce: 'Rebote',
|
||||
lifeLoss: 'Pérdida de vida'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Boundary Types
|
||||
// ========================================
|
||||
boundaryType: {
|
||||
none: 'Ninguno',
|
||||
rectangle: 'Rectángulo',
|
||||
circle: 'Círculo'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Behaviors
|
||||
// ========================================
|
||||
collisionBehavior: {
|
||||
kill: 'Eliminar',
|
||||
bounce: 'Rebotar',
|
||||
wrap: 'Envolver'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Force Field Module
|
||||
// ========================================
|
||||
forceField: {
|
||||
type: 'Tipo',
|
||||
strength: 'Fuerza',
|
||||
directionX: 'Dirección X',
|
||||
directionY: 'Dirección Y',
|
||||
centerX: 'Centro X',
|
||||
centerY: 'Centro Y',
|
||||
range: 'Rango',
|
||||
falloff: 'Caída'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Force Field Types
|
||||
// ========================================
|
||||
forceFieldType: {
|
||||
wind: 'Viento',
|
||||
point: 'Punto',
|
||||
vortex: 'Vórtice',
|
||||
turbulence: 'Turbulencia'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Curve Editor
|
||||
// ========================================
|
||||
curve: {
|
||||
deletePoint: 'Eliminar punto',
|
||||
constant: 'Valor constante',
|
||||
fadeIn: 'Aparecer',
|
||||
fadeOut: 'Desvanecer',
|
||||
bellCurve: 'Curva campana',
|
||||
uCurve: 'Curva U'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Gradient Editor
|
||||
// ========================================
|
||||
gradient: {
|
||||
deleteStop: 'Eliminar parada'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Texture Picker
|
||||
// ========================================
|
||||
texturePicker: {
|
||||
browse: 'Examinar...',
|
||||
clear: 'Limpiar'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: 'Archivo guardado: {{path}}',
|
||||
fileSaveFailed: 'Error al guardar archivo',
|
||||
fileOpened: 'Archivo abierto: {{path}}',
|
||||
fileOpenFailed: 'Error al abrir archivo'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
selectTexture: 'Seleccionar imagen de textura',
|
||||
selectParticleFile: 'Seleccionar archivo de partículas',
|
||||
saveParticleFile: 'Guardar archivo de partículas'
|
||||
}
|
||||
};
|
||||
11
packages/particle-editor/src/locales/index.ts
Normal file
11
packages/particle-editor/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Particle Editor Locales
|
||||
* 粒子编辑器语言包
|
||||
*
|
||||
* Export all locale translations for the particle editor plugin.
|
||||
* 导出粒子编辑器插件的所有语言翻译。
|
||||
*/
|
||||
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
267
packages/particle-editor/src/locales/zh.ts
Normal file
267
packages/particle-editor/src/locales/zh.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Chinese translations for Particle Editor
|
||||
* 粒子编辑器中文翻译
|
||||
*/
|
||||
export const zh = {
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: '粒子编辑器',
|
||||
noFileOpen: '没有打开的粒子文件',
|
||||
dropToOpen: '拖放 .particle 文件到这里或使用打开按钮'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
play: '播放',
|
||||
pause: '暂停',
|
||||
restart: '重新开始',
|
||||
save: '保存',
|
||||
open: '打开',
|
||||
maximize: '最大化预览',
|
||||
minimize: '最小化预览',
|
||||
followMouse: '跟随鼠标',
|
||||
resetPosition: '重置位置'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Sections
|
||||
// ========================================
|
||||
sections: {
|
||||
basic: '基础',
|
||||
emission: '发射',
|
||||
particle: '粒子',
|
||||
color: '颜色',
|
||||
modules: '模块',
|
||||
presets: '预设'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Basic Properties
|
||||
// ========================================
|
||||
basic: {
|
||||
name: '名称',
|
||||
texture: '纹理',
|
||||
maxParticles: '最大粒子数',
|
||||
looping: '循环',
|
||||
duration: '持续时间',
|
||||
prewarm: '预热',
|
||||
playSpeed: '播放速度',
|
||||
blendMode: '混合模式',
|
||||
space: '空间',
|
||||
particleSize: '粒子大小',
|
||||
sortOrder: '排序顺序'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blend Modes
|
||||
// ========================================
|
||||
blendMode: {
|
||||
normal: '普通',
|
||||
additive: '叠加',
|
||||
multiply: '乘法'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Simulation Space
|
||||
// ========================================
|
||||
space: {
|
||||
world: '世界',
|
||||
local: '本地'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Emission Properties
|
||||
// ========================================
|
||||
emission: {
|
||||
rate: '发射率',
|
||||
shape: '形状',
|
||||
radius: '半径',
|
||||
width: '宽度',
|
||||
height: '高度',
|
||||
coneAngle: '锥形角度'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Emission Shapes
|
||||
// ========================================
|
||||
shapes: {
|
||||
point: '点',
|
||||
circle: '圆形',
|
||||
ring: '环形',
|
||||
rectangle: '矩形',
|
||||
edge: '边缘',
|
||||
line: '线',
|
||||
cone: '锥形'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Particle Properties
|
||||
// ========================================
|
||||
particle: {
|
||||
lifetime: '生命周期',
|
||||
speed: '速度',
|
||||
direction: '方向',
|
||||
spread: '散布',
|
||||
scale: '缩放',
|
||||
gravity: '重力'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Color Properties
|
||||
// ========================================
|
||||
color: {
|
||||
startColor: '起始颜色',
|
||||
startAlpha: '起始透明度',
|
||||
endAlpha: '结束透明度',
|
||||
endScale: '结束缩放'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Module Names
|
||||
// ========================================
|
||||
modules: {
|
||||
colorOverLifetime: '颜色随生命周期',
|
||||
sizeOverLifetime: '大小随生命周期',
|
||||
velocityOverLifetime: '速度随生命周期',
|
||||
rotationOverLifetime: '旋转随生命周期',
|
||||
noise: '噪声',
|
||||
collision: '碰撞',
|
||||
forceField: '力场'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Velocity Over Lifetime
|
||||
// ========================================
|
||||
velocity: {
|
||||
drag: '阻力',
|
||||
orbital: '轨道',
|
||||
radial: '径向'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Rotation Over Lifetime
|
||||
// ========================================
|
||||
rotation: {
|
||||
startMult: '起始倍数',
|
||||
endMult: '结束倍数',
|
||||
additional: '附加'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Noise Module
|
||||
// ========================================
|
||||
noise: {
|
||||
position: '位置',
|
||||
velocity: '速度',
|
||||
rotation: '旋转',
|
||||
frequency: '频率',
|
||||
scroll: '滚动'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Module
|
||||
// ========================================
|
||||
collision: {
|
||||
boundary: '边界',
|
||||
behavior: '行为',
|
||||
left: '左',
|
||||
right: '右',
|
||||
top: '上',
|
||||
bottom: '下',
|
||||
radius: '半径',
|
||||
bounce: '弹跳',
|
||||
lifeLoss: '生命损失'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Boundary Types
|
||||
// ========================================
|
||||
boundaryType: {
|
||||
none: '无',
|
||||
rectangle: '矩形',
|
||||
circle: '圆形'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Behaviors
|
||||
// ========================================
|
||||
collisionBehavior: {
|
||||
kill: '消灭',
|
||||
bounce: '弹跳',
|
||||
wrap: '环绕'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Force Field Module
|
||||
// ========================================
|
||||
forceField: {
|
||||
type: '类型',
|
||||
strength: '强度',
|
||||
directionX: '方向 X',
|
||||
directionY: '方向 Y',
|
||||
centerX: '中心 X',
|
||||
centerY: '中心 Y',
|
||||
range: '范围',
|
||||
falloff: '衰减'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Force Field Types
|
||||
// ========================================
|
||||
forceFieldType: {
|
||||
wind: '风',
|
||||
point: '点',
|
||||
vortex: '漩涡',
|
||||
turbulence: '湍流'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Curve Editor
|
||||
// ========================================
|
||||
curve: {
|
||||
deletePoint: '删除点',
|
||||
constant: '常量',
|
||||
fadeIn: '淡入',
|
||||
fadeOut: '淡出',
|
||||
bellCurve: '钟形曲线',
|
||||
uCurve: 'U 形曲线'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Gradient Editor
|
||||
// ========================================
|
||||
gradient: {
|
||||
deleteStop: '删除色标'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Texture Picker
|
||||
// ========================================
|
||||
texturePicker: {
|
||||
browse: '浏览...',
|
||||
clear: '清除'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: '文件已保存: {{path}}',
|
||||
fileSaveFailed: '保存文件失败',
|
||||
fileOpened: '文件已打开: {{path}}',
|
||||
fileOpenFailed: '打开文件失败'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
selectTexture: '选择纹理图片',
|
||||
selectParticleFile: '选择粒子文件',
|
||||
saveParticleFile: '保存粒子文件'
|
||||
}
|
||||
};
|
||||
@@ -31,6 +31,7 @@
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
73
packages/tilemap-editor/src/hooks/useTilemapLocale.ts
Normal file
73
packages/tilemap-editor/src/hooks/useTilemapLocale.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Tilemap Editor Locale Hook
|
||||
* 瓦片地图编辑器语言钩子
|
||||
*
|
||||
* Uses the unified plugin i18n infrastructure from editor-runtime.
|
||||
* 使用 editor-runtime 的统一插件国际化基础设施。
|
||||
*/
|
||||
import {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale
|
||||
} from '@esengine/editor-runtime';
|
||||
import { en, zh, es } from '../locales';
|
||||
import type { Locale, TranslationParams } from '@esengine/editor-core';
|
||||
|
||||
// Create translations bundle
|
||||
// 创建翻译包
|
||||
const translations = { en, zh, es };
|
||||
|
||||
/**
|
||||
* Hook for accessing tilemap editor translations
|
||||
* 访问瓦片地图编辑器翻译的 Hook
|
||||
*
|
||||
* Uses the unified createPluginLocale factory from editor-runtime.
|
||||
* 使用 editor-runtime 的统一 createPluginLocale 工厂。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { t, locale } = useTilemapLocale();
|
||||
* return <button title={t('toolbar.save')}>{t('toolbar.saveButton')}</button>;
|
||||
* ```
|
||||
*/
|
||||
export const useTilemapLocale = createPluginLocale(translations);
|
||||
|
||||
// Create non-React translator using the unified infrastructure
|
||||
// 使用统一基础设施创建非 React 翻译器
|
||||
const tilemapTranslator = createPluginTranslator(translations);
|
||||
|
||||
/**
|
||||
* Non-React translation function for tilemap editor
|
||||
* 瓦片地图编辑器的非 React 翻译函数
|
||||
*
|
||||
* Use this in services, utilities, and other non-React contexts.
|
||||
* 在服务、工具类和其他非 React 上下文中使用。
|
||||
*
|
||||
* @param key - Translation key | 翻译键
|
||||
* @param locale - Optional locale, defaults to current locale | 可选语言,默认使用当前语言
|
||||
* @param params - Optional interpolation parameters | 可选插值参数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit locale
|
||||
* translateTilemap('errors.notFound', 'zh');
|
||||
*
|
||||
* // With current locale (auto-detected)
|
||||
* translateTilemap('toolbar.save');
|
||||
*
|
||||
* // With parameters
|
||||
* translateTilemap('layers.layerCount', undefined, { count: 5 });
|
||||
* ```
|
||||
*/
|
||||
export function translateTilemap(
|
||||
key: string,
|
||||
locale?: Locale,
|
||||
params?: TranslationParams
|
||||
): string {
|
||||
const targetLocale = locale || getCurrentLocale();
|
||||
return tilemapTranslator(key, targetLocale, params);
|
||||
}
|
||||
|
||||
// Re-export for external use
|
||||
// 重新导出供外部使用
|
||||
export { getCurrentLocale } from '@esengine/editor-runtime';
|
||||
181
packages/tilemap-editor/src/locales/en.ts
Normal file
181
packages/tilemap-editor/src/locales/en.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* English translations for Tilemap Editor
|
||||
* 瓦片地图编辑器英文翻译
|
||||
*/
|
||||
export const en = {
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Tilemap Editor',
|
||||
noTilemapSelected: 'No tilemap selected',
|
||||
details: 'Details',
|
||||
search: 'Search'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Tools
|
||||
// ========================================
|
||||
tools: {
|
||||
tileMode: 'Tile editing mode',
|
||||
collisionMode: 'Collision editing mode',
|
||||
tile: 'Tile',
|
||||
collision: 'Collision',
|
||||
draw: 'Draw',
|
||||
drawTile: 'Draw tiles',
|
||||
drawCollision: 'Draw collision',
|
||||
eraser: 'Eraser',
|
||||
eraseTile: 'Erase tiles',
|
||||
eraseCollision: 'Erase collision',
|
||||
fill: 'Fill',
|
||||
fillTile: 'Fill tiles',
|
||||
fillCollision: 'Fill collision',
|
||||
rectangle: 'Rectangle',
|
||||
rectangleTile: 'Rectangle draw',
|
||||
rectangleCollision: 'Rectangle collision',
|
||||
select: 'Select',
|
||||
selectRegion: 'Select region'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Tileset
|
||||
// ========================================
|
||||
tileset: {
|
||||
activeTileset: 'Active Tileset',
|
||||
showGrid: 'Show grid',
|
||||
search: 'Search',
|
||||
none: '(None)',
|
||||
addTileset: '+ Add Tileset...',
|
||||
zoom: 'Zoom {{zoom}}:1',
|
||||
selector: 'Tileset Selector',
|
||||
selectTileset: 'Select Tileset',
|
||||
selected: 'Selected: {{width}}×{{height}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Mode
|
||||
// ========================================
|
||||
collisionMode: {
|
||||
title: 'Collision Edit Mode',
|
||||
drawHint: 'Use brush to draw collision areas',
|
||||
eraseHint: 'Use eraser to clear collision'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layers
|
||||
// ========================================
|
||||
layers: {
|
||||
title: 'Layers',
|
||||
addLayer: 'Add Layer',
|
||||
layerCount: 'Layers ({{count}})',
|
||||
layer: 'Layer',
|
||||
layerNumber: 'Layer {{number}}',
|
||||
editingCollision: 'Currently editing collision',
|
||||
drawingLayer: 'Currently drawing layer',
|
||||
moveUp: 'Move layer up',
|
||||
moveDown: 'Move layer down',
|
||||
delete: 'Delete layer',
|
||||
duplicate: 'Duplicate layer',
|
||||
hide: 'Hide layer',
|
||||
show: 'Show layer'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layer Properties
|
||||
// ========================================
|
||||
layerProperties: {
|
||||
title: 'Selected Layer',
|
||||
name: 'Name',
|
||||
editName: 'Double-click to edit name',
|
||||
hideInEditor: 'Hide in editor',
|
||||
hideInGame: 'Hide in game',
|
||||
opacity: 'Layer opacity',
|
||||
collision: 'Layer collision',
|
||||
overrideThickness: 'Override collision thickness',
|
||||
overrideOffset: 'Override collision offset',
|
||||
thicknessOverride: 'Collision thickness override',
|
||||
offsetOverride: 'Collision offset override',
|
||||
color: 'Layer color',
|
||||
material: '{{name}} Material'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Configuration
|
||||
// ========================================
|
||||
config: {
|
||||
title: 'Configuration',
|
||||
mapWidth: 'Map width',
|
||||
mapHeight: 'Map height',
|
||||
tileWidth: 'Tile width',
|
||||
tileHeight: 'Tile height',
|
||||
pixelsPerUnit: 'Pixels per unit',
|
||||
separateByLayer: 'Separate by layer'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layer Materials
|
||||
// ========================================
|
||||
materials: {
|
||||
title: 'Layer Materials',
|
||||
default: 'Default Material',
|
||||
selectMaterial: 'Click to select material',
|
||||
copyPath: 'Copy path',
|
||||
clear: 'Clear'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Advanced
|
||||
// ========================================
|
||||
advanced: {
|
||||
title: 'Advanced',
|
||||
projection: 'Projection mode',
|
||||
orthographic: 'Orthographic',
|
||||
isometric: 'Isometric',
|
||||
hexagonal: 'Hexagonal',
|
||||
hexSideLength: 'Hex side length',
|
||||
backgroundColor: 'Background color',
|
||||
tileGridColor: 'Tile grid color',
|
||||
multiTileGridColor: 'Multi-tile grid color',
|
||||
multiTileGridWidth: 'Multi-tile grid width'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Settings
|
||||
// ========================================
|
||||
collisionSettings: {
|
||||
title: 'Collision',
|
||||
showCollision: 'Show collision'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
toggleGrid: 'Toggle grid',
|
||||
showCollision: 'Show collision',
|
||||
save: 'Save (Ctrl+S)',
|
||||
saveButton: 'Save',
|
||||
zoomOut: 'Zoom out',
|
||||
zoomIn: 'Zoom in',
|
||||
resetView: 'Reset view',
|
||||
cells: ' cells'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
selectTilesetImage: 'Select tileset image',
|
||||
selectLayerMaterial: 'Select layer material',
|
||||
searchAssets: 'Search assets...'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Animation Editor
|
||||
// ========================================
|
||||
animation: {
|
||||
frames: 'Animation Frames',
|
||||
deleteFrame: 'Delete frame',
|
||||
addFrameHint: 'Click a tile to add frame'
|
||||
}
|
||||
};
|
||||
181
packages/tilemap-editor/src/locales/es.ts
Normal file
181
packages/tilemap-editor/src/locales/es.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Spanish translations for Tilemap Editor
|
||||
* Traducciones en español del editor de mapas de tiles
|
||||
*/
|
||||
export const es = {
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Editor de Tilemap',
|
||||
noTilemapSelected: 'Ningún tilemap seleccionado',
|
||||
details: 'Detalles',
|
||||
search: 'Buscar'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Tools
|
||||
// ========================================
|
||||
tools: {
|
||||
tileMode: 'Modo de edición de tiles',
|
||||
collisionMode: 'Modo de edición de colisión',
|
||||
tile: 'Tile',
|
||||
collision: 'Colisión',
|
||||
draw: 'Dibujar',
|
||||
drawTile: 'Dibujar tiles',
|
||||
drawCollision: 'Dibujar colisión',
|
||||
eraser: 'Borrador',
|
||||
eraseTile: 'Borrar tiles',
|
||||
eraseCollision: 'Borrar colisión',
|
||||
fill: 'Rellenar',
|
||||
fillTile: 'Rellenar tiles',
|
||||
fillCollision: 'Rellenar colisión',
|
||||
rectangle: 'Rectángulo',
|
||||
rectangleTile: 'Dibujo rectangular',
|
||||
rectangleCollision: 'Colisión rectangular',
|
||||
select: 'Seleccionar',
|
||||
selectRegion: 'Seleccionar región'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Tileset
|
||||
// ========================================
|
||||
tileset: {
|
||||
activeTileset: 'Tileset activo',
|
||||
showGrid: 'Mostrar cuadrícula',
|
||||
search: 'Buscar',
|
||||
none: '(Ninguno)',
|
||||
addTileset: '+ Agregar Tileset...',
|
||||
zoom: 'Zoom {{zoom}}:1',
|
||||
selector: 'Selector de Tileset',
|
||||
selectTileset: 'Seleccionar Tileset',
|
||||
selected: 'Seleccionado: {{width}}×{{height}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Mode
|
||||
// ========================================
|
||||
collisionMode: {
|
||||
title: 'Modo de edición de colisión',
|
||||
drawHint: 'Use el pincel para dibujar áreas de colisión',
|
||||
eraseHint: 'Use el borrador para eliminar colisión'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layers
|
||||
// ========================================
|
||||
layers: {
|
||||
title: 'Capas',
|
||||
addLayer: 'Agregar capa',
|
||||
layerCount: 'Capas ({{count}})',
|
||||
layer: 'Capa',
|
||||
layerNumber: 'Capa {{number}}',
|
||||
editingCollision: 'Editando colisión actualmente',
|
||||
drawingLayer: 'Dibujando en capa actualmente',
|
||||
moveUp: 'Mover capa arriba',
|
||||
moveDown: 'Mover capa abajo',
|
||||
delete: 'Eliminar capa',
|
||||
duplicate: 'Duplicar capa',
|
||||
hide: 'Ocultar capa',
|
||||
show: 'Mostrar capa'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layer Properties
|
||||
// ========================================
|
||||
layerProperties: {
|
||||
title: 'Capa seleccionada',
|
||||
name: 'Nombre',
|
||||
editName: 'Doble clic para editar nombre',
|
||||
hideInEditor: 'Ocultar en editor',
|
||||
hideInGame: 'Ocultar en juego',
|
||||
opacity: 'Opacidad de capa',
|
||||
collision: 'Colisión de capa',
|
||||
overrideThickness: 'Anular grosor de colisión',
|
||||
overrideOffset: 'Anular desplazamiento de colisión',
|
||||
thicknessOverride: 'Anulación de grosor de colisión',
|
||||
offsetOverride: 'Anulación de desplazamiento de colisión',
|
||||
color: 'Color de capa',
|
||||
material: 'Material {{name}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Configuration
|
||||
// ========================================
|
||||
config: {
|
||||
title: 'Configuración',
|
||||
mapWidth: 'Ancho del mapa',
|
||||
mapHeight: 'Alto del mapa',
|
||||
tileWidth: 'Ancho de tile',
|
||||
tileHeight: 'Alto de tile',
|
||||
pixelsPerUnit: 'Píxeles por unidad',
|
||||
separateByLayer: 'Separar por capa'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layer Materials
|
||||
// ========================================
|
||||
materials: {
|
||||
title: 'Materiales de capa',
|
||||
default: 'Material predeterminado',
|
||||
selectMaterial: 'Clic para seleccionar material',
|
||||
copyPath: 'Copiar ruta',
|
||||
clear: 'Limpiar'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Advanced
|
||||
// ========================================
|
||||
advanced: {
|
||||
title: 'Avanzado',
|
||||
projection: 'Modo de proyección',
|
||||
orthographic: 'Ortográfico',
|
||||
isometric: 'Isométrico',
|
||||
hexagonal: 'Hexagonal',
|
||||
hexSideLength: 'Longitud del lado hexagonal',
|
||||
backgroundColor: 'Color de fondo',
|
||||
tileGridColor: 'Color de cuadrícula de tiles',
|
||||
multiTileGridColor: 'Color de cuadrícula multi-tile',
|
||||
multiTileGridWidth: 'Ancho de cuadrícula multi-tile'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Settings
|
||||
// ========================================
|
||||
collisionSettings: {
|
||||
title: 'Colisión',
|
||||
showCollision: 'Mostrar colisión'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
toggleGrid: 'Alternar cuadrícula',
|
||||
showCollision: 'Mostrar colisión',
|
||||
save: 'Guardar (Ctrl+S)',
|
||||
saveButton: 'Guardar',
|
||||
zoomOut: 'Alejar',
|
||||
zoomIn: 'Acercar',
|
||||
resetView: 'Restablecer vista',
|
||||
cells: ' celdas'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
selectTilesetImage: 'Seleccionar imagen de tileset',
|
||||
selectLayerMaterial: 'Seleccionar material de capa',
|
||||
searchAssets: 'Buscar assets...'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Animation Editor
|
||||
// ========================================
|
||||
animation: {
|
||||
frames: 'Fotogramas de animación',
|
||||
deleteFrame: 'Eliminar fotograma',
|
||||
addFrameHint: 'Clic en un tile para agregar fotograma'
|
||||
}
|
||||
};
|
||||
11
packages/tilemap-editor/src/locales/index.ts
Normal file
11
packages/tilemap-editor/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Tilemap Editor Locales
|
||||
* 瓦片地图编辑器语言包
|
||||
*
|
||||
* Export all locale translations for the tilemap editor plugin.
|
||||
* 导出瓦片地图编辑器插件的所有语言翻译。
|
||||
*/
|
||||
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
181
packages/tilemap-editor/src/locales/zh.ts
Normal file
181
packages/tilemap-editor/src/locales/zh.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Chinese translations for Tilemap Editor
|
||||
* 瓦片地图编辑器中文翻译
|
||||
*/
|
||||
export const zh = {
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: '瓦片地图编辑器',
|
||||
noTilemapSelected: '未选择瓦片地图',
|
||||
details: '细节',
|
||||
search: '搜索'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Tools
|
||||
// ========================================
|
||||
tools: {
|
||||
tileMode: '瓦片编辑模式',
|
||||
collisionMode: '碰撞编辑模式',
|
||||
tile: '瓦片',
|
||||
collision: '碰撞',
|
||||
draw: '绘制',
|
||||
drawTile: '绘制瓦片',
|
||||
drawCollision: '绘制碰撞',
|
||||
eraser: '橡皮擦',
|
||||
eraseTile: '擦除瓦片',
|
||||
eraseCollision: '擦除碰撞',
|
||||
fill: '填充',
|
||||
fillTile: '填充瓦片',
|
||||
fillCollision: '填充碰撞',
|
||||
rectangle: '矩形',
|
||||
rectangleTile: '矩形绘制',
|
||||
rectangleCollision: '矩形碰撞',
|
||||
select: '选择',
|
||||
selectRegion: '选择区域'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Tileset
|
||||
// ========================================
|
||||
tileset: {
|
||||
activeTileset: '活跃瓦片集',
|
||||
showGrid: '显示网格',
|
||||
search: '搜索',
|
||||
none: '(无)',
|
||||
addTileset: '+ 添加瓦片集...',
|
||||
zoom: '缩放 {{zoom}}:1',
|
||||
selector: '瓦片集选择器',
|
||||
selectTileset: '选择瓦片集',
|
||||
selected: '已选择: {{width}}×{{height}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Mode
|
||||
// ========================================
|
||||
collisionMode: {
|
||||
title: '碰撞编辑模式',
|
||||
drawHint: '使用画笔绘制碰撞区域',
|
||||
eraseHint: '使用橡皮擦清除碰撞'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layers
|
||||
// ========================================
|
||||
layers: {
|
||||
title: '图层',
|
||||
addLayer: '添加图层',
|
||||
layerCount: '图层 ({{count}})',
|
||||
layer: '图层',
|
||||
layerNumber: '图层 {{number}}',
|
||||
editingCollision: '当前编辑碰撞',
|
||||
drawingLayer: '当前绘制图层',
|
||||
moveUp: '上移图层',
|
||||
moveDown: '下移图层',
|
||||
delete: '删除图层',
|
||||
duplicate: '复制图层',
|
||||
hide: '隐藏图层',
|
||||
show: '显示图层'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layer Properties
|
||||
// ========================================
|
||||
layerProperties: {
|
||||
title: '选定层',
|
||||
name: '名称',
|
||||
editName: '双击编辑名称',
|
||||
hideInEditor: '编辑器中隐藏',
|
||||
hideInGame: '游戏中隐藏',
|
||||
opacity: '图层透明度',
|
||||
collision: '图层碰撞',
|
||||
overrideThickness: '重载碰撞厚度',
|
||||
overrideOffset: '重载碰撞偏移',
|
||||
thicknessOverride: '碰撞厚度重载',
|
||||
offsetOverride: '碰撞偏移重载',
|
||||
color: '图层颜色',
|
||||
material: '{{name}} 材质'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Configuration
|
||||
// ========================================
|
||||
config: {
|
||||
title: '配置',
|
||||
mapWidth: '地图宽度',
|
||||
mapHeight: '地图高度',
|
||||
tileWidth: '瓦片宽度',
|
||||
tileHeight: '瓦片高度',
|
||||
pixelsPerUnit: '逻辑单位像素',
|
||||
separateByLayer: '逐图层分隔'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Layer Materials
|
||||
// ========================================
|
||||
materials: {
|
||||
title: '图层材质',
|
||||
default: '默认材质',
|
||||
selectMaterial: '点击选择材质',
|
||||
copyPath: '复制路径',
|
||||
clear: '清除'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Advanced
|
||||
// ========================================
|
||||
advanced: {
|
||||
title: '高级',
|
||||
projection: '投射模式',
|
||||
orthographic: '正交',
|
||||
isometric: '等轴测',
|
||||
hexagonal: '六方',
|
||||
hexSideLength: '六方格边长度',
|
||||
backgroundColor: '背景颜色',
|
||||
tileGridColor: '瓦片网格颜色',
|
||||
multiTileGridColor: '多瓦片网格颜色',
|
||||
multiTileGridWidth: '多瓦片网格宽度'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Collision Settings
|
||||
// ========================================
|
||||
collisionSettings: {
|
||||
title: '碰撞',
|
||||
showCollision: '显示碰撞'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
toggleGrid: '切换网格',
|
||||
showCollision: '显示碰撞',
|
||||
save: '保存 (Ctrl+S)',
|
||||
saveButton: '保存',
|
||||
zoomOut: '缩小',
|
||||
zoomIn: '放大',
|
||||
resetView: '重置视图',
|
||||
cells: ' 格'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
selectTilesetImage: '选择瓦片集图片',
|
||||
selectLayerMaterial: '选择图层材质',
|
||||
searchAssets: '搜索资产...'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Animation Editor
|
||||
// ========================================
|
||||
animation: {
|
||||
frames: '动画帧',
|
||||
deleteFrame: '删除帧',
|
||||
addFrameHint: '点击瓦片添加帧'
|
||||
}
|
||||
};
|
||||
@@ -29,6 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/editor-core": "workspace:*",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
61
packages/ui-editor/src/hooks/useUILocale.ts
Normal file
61
packages/ui-editor/src/hooks/useUILocale.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* UI Editor Locale Hook
|
||||
* UI 编辑器语言钩子
|
||||
*
|
||||
* Uses the unified plugin i18n infrastructure from editor-runtime.
|
||||
* 使用 editor-runtime 的统一插件国际化基础设施。
|
||||
*/
|
||||
import {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale
|
||||
} from '@esengine/editor-runtime';
|
||||
import { en, zh, es } from '../locales';
|
||||
import type { Locale, TranslationParams } from '@esengine/editor-core';
|
||||
|
||||
// Create translations bundle
|
||||
// 创建翻译包
|
||||
const translations = { en, zh, es };
|
||||
|
||||
/**
|
||||
* Hook for accessing UI editor translations
|
||||
* 访问 UI 编辑器翻译的 Hook
|
||||
*
|
||||
* Uses the unified createPluginLocale factory from editor-runtime.
|
||||
* 使用 editor-runtime 的统一 createPluginLocale 工厂。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { t, locale } = useUILocale();
|
||||
* return <label>{t('inspector.position')}</label>;
|
||||
* ```
|
||||
*/
|
||||
export const useUILocale = createPluginLocale(translations);
|
||||
|
||||
// Create non-React translator using the unified infrastructure
|
||||
// 使用统一基础设施创建非 React 翻译器
|
||||
const uiTranslator = createPluginTranslator(translations);
|
||||
|
||||
/**
|
||||
* Non-React translation function for UI editor
|
||||
* UI 编辑器的非 React 翻译函数
|
||||
*
|
||||
* Use this in services, utilities, and other non-React contexts.
|
||||
* 在服务、工具类和其他非 React 上下文中使用。
|
||||
*
|
||||
* @param key - Translation key | 翻译键
|
||||
* @param locale - Optional locale, defaults to current locale | 可选语言,默认使用当前语言
|
||||
* @param params - Optional interpolation parameters | 可选插值参数
|
||||
*/
|
||||
export function translateUI(
|
||||
key: string,
|
||||
locale?: Locale,
|
||||
params?: TranslationParams
|
||||
): string {
|
||||
const targetLocale = locale || getCurrentLocale();
|
||||
return uiTranslator(key, targetLocale, params);
|
||||
}
|
||||
|
||||
// Re-export for external use
|
||||
// 重新导出供外部使用
|
||||
export { getCurrentLocale } from '@esengine/editor-runtime';
|
||||
52
packages/ui-editor/src/locales/en.ts
Normal file
52
packages/ui-editor/src/locales/en.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* English translations for UI Editor
|
||||
* UI 编辑器英文翻译
|
||||
*/
|
||||
export const en = {
|
||||
// ========================================
|
||||
// Inspector
|
||||
// ========================================
|
||||
inspector: {
|
||||
title: 'UITransform Inspector',
|
||||
anchor: 'Anchor',
|
||||
stretch: 'Stretch',
|
||||
position: 'Position',
|
||||
size: 'Size',
|
||||
anchorMin: 'Anchor Min',
|
||||
anchorMax: 'Anchor Max',
|
||||
pivot: 'Pivot',
|
||||
rotation: 'Rotation',
|
||||
scale: 'Scale',
|
||||
zIndex: 'Z Index',
|
||||
alpha: 'Alpha',
|
||||
visible: 'Visible'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Anchor Presets
|
||||
// ========================================
|
||||
anchorPresets: {
|
||||
topLeft: 'Top Left',
|
||||
topCenter: 'Top Center',
|
||||
topRight: 'Top Right',
|
||||
middleLeft: 'Middle Left',
|
||||
middleCenter: 'Middle Center',
|
||||
middleRight: 'Middle Right',
|
||||
bottomLeft: 'Bottom Left',
|
||||
bottomCenter: 'Bottom Center',
|
||||
bottomRight: 'Bottom Right',
|
||||
stretchAll: 'Stretch All'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Components
|
||||
// ========================================
|
||||
components: {
|
||||
uiTransform: 'UI Transform',
|
||||
uiRender: 'UI Render',
|
||||
uiText: 'UI Text',
|
||||
uiButton: 'UI Button',
|
||||
uiLayout: 'UI Layout',
|
||||
uiInteractable: 'UI Interactable'
|
||||
}
|
||||
};
|
||||
52
packages/ui-editor/src/locales/es.ts
Normal file
52
packages/ui-editor/src/locales/es.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Spanish translations for UI Editor
|
||||
* Traducciones en español del editor de UI
|
||||
*/
|
||||
export const es = {
|
||||
// ========================================
|
||||
// Inspector
|
||||
// ========================================
|
||||
inspector: {
|
||||
title: 'Inspector UITransform',
|
||||
anchor: 'Ancla',
|
||||
stretch: 'Estirar',
|
||||
position: 'Posición',
|
||||
size: 'Tamaño',
|
||||
anchorMin: 'Ancla Mín',
|
||||
anchorMax: 'Ancla Máx',
|
||||
pivot: 'Pivote',
|
||||
rotation: 'Rotación',
|
||||
scale: 'Escala',
|
||||
zIndex: 'Índice Z',
|
||||
alpha: 'Alfa',
|
||||
visible: 'Visible'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Anchor Presets
|
||||
// ========================================
|
||||
anchorPresets: {
|
||||
topLeft: 'Superior Izquierda',
|
||||
topCenter: 'Superior Centro',
|
||||
topRight: 'Superior Derecha',
|
||||
middleLeft: 'Centro Izquierda',
|
||||
middleCenter: 'Centro',
|
||||
middleRight: 'Centro Derecha',
|
||||
bottomLeft: 'Inferior Izquierda',
|
||||
bottomCenter: 'Inferior Centro',
|
||||
bottomRight: 'Inferior Derecha',
|
||||
stretchAll: 'Estirar Todo'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Components
|
||||
// ========================================
|
||||
components: {
|
||||
uiTransform: 'UI Transform',
|
||||
uiRender: 'UI Render',
|
||||
uiText: 'UI Text',
|
||||
uiButton: 'UI Button',
|
||||
uiLayout: 'UI Layout',
|
||||
uiInteractable: 'UI Interactable'
|
||||
}
|
||||
};
|
||||
11
packages/ui-editor/src/locales/index.ts
Normal file
11
packages/ui-editor/src/locales/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* UI Editor Locales
|
||||
* UI 编辑器语言包
|
||||
*
|
||||
* Export all locale translations for the UI editor plugin.
|
||||
* 导出 UI 编辑器插件的所有语言翻译。
|
||||
*/
|
||||
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
52
packages/ui-editor/src/locales/zh.ts
Normal file
52
packages/ui-editor/src/locales/zh.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Chinese translations for UI Editor
|
||||
* UI 编辑器中文翻译
|
||||
*/
|
||||
export const zh = {
|
||||
// ========================================
|
||||
// Inspector
|
||||
// ========================================
|
||||
inspector: {
|
||||
title: 'UI变换检查器',
|
||||
anchor: '锚点',
|
||||
stretch: '拉伸',
|
||||
position: '位置',
|
||||
size: '大小',
|
||||
anchorMin: '锚点最小值',
|
||||
anchorMax: '锚点最大值',
|
||||
pivot: '轴心',
|
||||
rotation: '旋转',
|
||||
scale: '缩放',
|
||||
zIndex: 'Z 顺序',
|
||||
alpha: '透明度',
|
||||
visible: '可见'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Anchor Presets
|
||||
// ========================================
|
||||
anchorPresets: {
|
||||
topLeft: '左上',
|
||||
topCenter: '上中',
|
||||
topRight: '右上',
|
||||
middleLeft: '左中',
|
||||
middleCenter: '中心',
|
||||
middleRight: '右中',
|
||||
bottomLeft: '左下',
|
||||
bottomCenter: '下中',
|
||||
bottomRight: '右下',
|
||||
stretchAll: '拉伸全部'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Components
|
||||
// ========================================
|
||||
components: {
|
||||
uiTransform: 'UI 变换',
|
||||
uiRender: 'UI 渲染',
|
||||
uiText: 'UI 文本',
|
||||
uiButton: 'UI 按钮',
|
||||
uiLayout: 'UI 布局',
|
||||
uiInteractable: 'UI 交互'
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user