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

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

## 主要改动

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

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

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

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

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

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

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

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

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

View File

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