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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user