国际化系统
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Core, Scene } from '@esengine/ecs-framework';
|
import { Core, Scene } from '@esengine/ecs-framework';
|
||||||
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry } from '@esengine/editor-core';
|
import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService } from '@esengine/editor-core';
|
||||||
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin';
|
||||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||||
import { EntityInspector } from './components/EntityInspector';
|
import { EntityInspector } from './components/EntityInspector';
|
||||||
@@ -8,20 +8,29 @@ import { TauriAPI } from './api/tauri';
|
|||||||
import { TransformComponent } from './example-components/TransformComponent';
|
import { TransformComponent } from './example-components/TransformComponent';
|
||||||
import { SpriteComponent } from './example-components/SpriteComponent';
|
import { SpriteComponent } from './example-components/SpriteComponent';
|
||||||
import { RigidBodyComponent } from './example-components/RigidBodyComponent';
|
import { RigidBodyComponent } from './example-components/RigidBodyComponent';
|
||||||
|
import { useLocale } from './hooks/useLocale';
|
||||||
|
import { en, zh } from './locales';
|
||||||
import './styles/App.css';
|
import './styles/App.css';
|
||||||
|
|
||||||
|
// 在 App 组件外部初始化 Core 和基础服务
|
||||||
|
Core.create({ debug: true });
|
||||||
|
|
||||||
|
const localeService = new LocaleService();
|
||||||
|
localeService.registerTranslations('en', en);
|
||||||
|
localeService.registerTranslations('zh', zh);
|
||||||
|
Core.services.registerInstance(LocaleService, localeService);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [initialized, setInitialized] = useState(false);
|
const [initialized, setInitialized] = useState(false);
|
||||||
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
const [pluginManager, setPluginManager] = useState<EditorPluginManager | null>(null);
|
||||||
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
const [entityStore, setEntityStore] = useState<EntityStoreService | null>(null);
|
||||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||||
const [status, setStatus] = useState('Initializing...');
|
const { t, locale, changeLocale } = useLocale();
|
||||||
|
const [status, setStatus] = useState(t('header.status.initializing'));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeEditor = async () => {
|
const initializeEditor = async () => {
|
||||||
try {
|
try {
|
||||||
const coreInstance = Core.create({ debug: true });
|
|
||||||
|
|
||||||
const editorScene = new Scene();
|
const editorScene = new Scene();
|
||||||
Core.setScene(editorScene);
|
Core.setScene(editorScene);
|
||||||
|
|
||||||
@@ -59,7 +68,7 @@ function App() {
|
|||||||
Core.services.registerInstance(ComponentRegistry, componentRegistry);
|
Core.services.registerInstance(ComponentRegistry, componentRegistry);
|
||||||
|
|
||||||
const pluginMgr = new EditorPluginManager();
|
const pluginMgr = new EditorPluginManager();
|
||||||
pluginMgr.initialize(coreInstance, Core.services);
|
pluginMgr.initialize(Core, Core.services);
|
||||||
|
|
||||||
await pluginMgr.installEditor(new SceneInspectorPlugin());
|
await pluginMgr.installEditor(new SceneInspectorPlugin());
|
||||||
|
|
||||||
@@ -70,18 +79,14 @@ function App() {
|
|||||||
setPluginManager(pluginMgr);
|
setPluginManager(pluginMgr);
|
||||||
setEntityStore(entityStore);
|
setEntityStore(entityStore);
|
||||||
setMessageHub(messageHub);
|
setMessageHub(messageHub);
|
||||||
setStatus('Editor Ready');
|
setStatus(t('header.status.ready'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize editor:', error);
|
console.error('Failed to initialize editor:', error);
|
||||||
setStatus('Initialization Failed');
|
setStatus(t('header.status.failed'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
initializeEditor();
|
initializeEditor();
|
||||||
|
|
||||||
return () => {
|
|
||||||
Core.destroy();
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateEntity = () => {
|
const handleCreateEntity = () => {
|
||||||
@@ -102,16 +107,24 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLocaleChange = () => {
|
||||||
|
const newLocale = locale === 'en' ? 'zh' : 'en';
|
||||||
|
changeLocale(newLocale);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="editor-container">
|
<div className="editor-container">
|
||||||
<div className="editor-header">
|
<div className="editor-header">
|
||||||
<h1>ECS Framework Editor</h1>
|
<h1>{t('app.title')}</h1>
|
||||||
<div className="header-toolbar">
|
<div className="header-toolbar">
|
||||||
<button onClick={handleCreateEntity} disabled={!initialized} className="toolbar-btn">
|
<button onClick={handleCreateEntity} disabled={!initialized} className="toolbar-btn">
|
||||||
➕ Create Entity
|
{t('header.toolbar.createEntity')}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleDeleteEntity} disabled={!entityStore?.getSelectedEntity()} className="toolbar-btn">
|
<button onClick={handleDeleteEntity} disabled={!entityStore?.getSelectedEntity()} className="toolbar-btn">
|
||||||
🗑️ Delete Entity
|
{t('header.toolbar.deleteEntity')}
|
||||||
|
</button>
|
||||||
|
<button onClick={handleLocaleChange} className="toolbar-btn locale-btn" title={locale === 'en' ? '切换到中文' : 'Switch to English'}>
|
||||||
|
{locale === 'en' ? '中' : 'EN'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span className="status">{status}</span>
|
<span className="status">{status}</span>
|
||||||
@@ -128,13 +141,13 @@ function App() {
|
|||||||
|
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
<div className="viewport">
|
<div className="viewport">
|
||||||
<h3>Viewport</h3>
|
<h3>{t('viewport.title')}</h3>
|
||||||
<p>Scene viewport will appear here</p>
|
<p>{t('viewport.placeholder')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bottom-panel">
|
<div className="bottom-panel">
|
||||||
<h4>Console</h4>
|
<h4>{t('console.title')}</h4>
|
||||||
<p>Console output will appear here</p>
|
<p>{t('console.placeholder')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,9 +161,9 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-footer">
|
<div className="editor-footer">
|
||||||
<span>Plugins: {pluginManager?.getAllEditorPlugins().length ?? 0}</span>
|
<span>{t('footer.plugins')}: {pluginManager?.getAllEditorPlugins().length ?? 0}</span>
|
||||||
<span>Entities: {entityStore?.getAllEntities().length ?? 0}</span>
|
<span>{t('footer.entities')}: {entityStore?.getAllEntities().length ?? 0}</span>
|
||||||
<span>Core: {initialized ? 'Active' : 'Inactive'}</span>
|
<span>{t('footer.core')}: {initialized ? t('footer.active') : t('footer.inactive')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Entity } from '@esengine/ecs-framework';
|
import { Entity } from '@esengine/ecs-framework';
|
||||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||||
|
import { useLocale } from '../hooks/useLocale';
|
||||||
import '../styles/SceneHierarchy.css';
|
import '../styles/SceneHierarchy.css';
|
||||||
|
|
||||||
interface SceneHierarchyProps {
|
interface SceneHierarchyProps {
|
||||||
@@ -11,6 +12,7 @@ interface SceneHierarchyProps {
|
|||||||
export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) {
|
export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps) {
|
||||||
const [entities, setEntities] = useState<Entity[]>([]);
|
const [entities, setEntities] = useState<Entity[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateEntities = () => {
|
const updateEntities = () => {
|
||||||
@@ -43,11 +45,11 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
|||||||
return (
|
return (
|
||||||
<div className="scene-hierarchy">
|
<div className="scene-hierarchy">
|
||||||
<div className="hierarchy-header">
|
<div className="hierarchy-header">
|
||||||
<h3>Scene Hierarchy</h3>
|
<h3>{t('hierarchy.title')}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="hierarchy-content">
|
<div className="hierarchy-content">
|
||||||
{entities.length === 0 ? (
|
{entities.length === 0 ? (
|
||||||
<div className="empty-state">No entities in scene</div>
|
<div className="empty-state">{t('hierarchy.empty')}</div>
|
||||||
) : (
|
) : (
|
||||||
<ul className="entity-list">
|
<ul className="entity-list">
|
||||||
{entities.map(entity => (
|
{entities.map(entity => (
|
||||||
|
|||||||
30
packages/editor-app/src/hooks/useLocale.ts
Normal file
30
packages/editor-app/src/hooks/useLocale.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import { LocaleService, type Locale } from '@esengine/editor-core';
|
||||||
|
|
||||||
|
export function useLocale() {
|
||||||
|
const localeService = Core.services.resolve(LocaleService);
|
||||||
|
const [locale, setLocale] = useState<Locale>(localeService.getCurrentLocale());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = localeService.onChange((newLocale) => {
|
||||||
|
setLocale(newLocale);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
}, [localeService]);
|
||||||
|
|
||||||
|
const t = (key: string, fallback?: string) => {
|
||||||
|
return localeService.t(key, fallback);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeLocale = (newLocale: Locale) => {
|
||||||
|
localeService.setLocale(newLocale);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
t,
|
||||||
|
changeLocale
|
||||||
|
};
|
||||||
|
}
|
||||||
63
packages/editor-app/src/locales/en.ts
Normal file
63
packages/editor-app/src/locales/en.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { Translations } from '@esengine/editor-core';
|
||||||
|
|
||||||
|
export const en: Translations = {
|
||||||
|
app: {
|
||||||
|
title: 'ECS Framework Editor'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
toolbar: {
|
||||||
|
createEntity: 'Create Entity',
|
||||||
|
deleteEntity: 'Delete Entity'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
initializing: 'Initializing...',
|
||||||
|
ready: 'Editor Ready',
|
||||||
|
failed: 'Initialization Failed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hierarchy: {
|
||||||
|
title: 'Scene Hierarchy',
|
||||||
|
empty: 'No entities',
|
||||||
|
loading: 'Loading...'
|
||||||
|
},
|
||||||
|
inspector: {
|
||||||
|
title: 'Inspector',
|
||||||
|
empty: 'No entity selected',
|
||||||
|
entityInfo: {
|
||||||
|
title: 'Entity Info',
|
||||||
|
id: 'ID',
|
||||||
|
name: 'Name',
|
||||||
|
enabled: 'Enabled',
|
||||||
|
yes: 'Yes',
|
||||||
|
no: 'No'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
title: 'Components',
|
||||||
|
empty: 'No components',
|
||||||
|
add: 'Add Component',
|
||||||
|
remove: 'Remove'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addComponent: {
|
||||||
|
title: 'Add Component',
|
||||||
|
search: 'Search components...',
|
||||||
|
empty: 'No available components',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
add: 'Add Component'
|
||||||
|
},
|
||||||
|
viewport: {
|
||||||
|
title: 'Viewport',
|
||||||
|
placeholder: 'Scene viewport will appear here'
|
||||||
|
},
|
||||||
|
console: {
|
||||||
|
title: 'Console',
|
||||||
|
placeholder: 'Console output will appear here'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
plugins: 'Plugins',
|
||||||
|
entities: 'Entities',
|
||||||
|
core: 'Core',
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive'
|
||||||
|
}
|
||||||
|
};
|
||||||
2
packages/editor-app/src/locales/index.ts
Normal file
2
packages/editor-app/src/locales/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { en } from './en';
|
||||||
|
export { zh } from './zh';
|
||||||
63
packages/editor-app/src/locales/zh.ts
Normal file
63
packages/editor-app/src/locales/zh.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import type { Translations } from '@esengine/editor-core';
|
||||||
|
|
||||||
|
export const zh: Translations = {
|
||||||
|
app: {
|
||||||
|
title: 'ECS 框架编辑器'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
toolbar: {
|
||||||
|
createEntity: '创建实体',
|
||||||
|
deleteEntity: '删除实体'
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
initializing: '初始化中...',
|
||||||
|
ready: '编辑器就绪',
|
||||||
|
failed: '初始化失败'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hierarchy: {
|
||||||
|
title: '场景层级',
|
||||||
|
empty: '无实体',
|
||||||
|
loading: '加载中...'
|
||||||
|
},
|
||||||
|
inspector: {
|
||||||
|
title: '检查器',
|
||||||
|
empty: '未选择实体',
|
||||||
|
entityInfo: {
|
||||||
|
title: '实体信息',
|
||||||
|
id: 'ID',
|
||||||
|
name: '名称',
|
||||||
|
enabled: '启用',
|
||||||
|
yes: '是',
|
||||||
|
no: '否'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
title: '组件',
|
||||||
|
empty: '无组件',
|
||||||
|
add: '添加组件',
|
||||||
|
remove: '移除'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addComponent: {
|
||||||
|
title: '添加组件',
|
||||||
|
search: '搜索组件...',
|
||||||
|
empty: '无可用组件',
|
||||||
|
cancel: '取消',
|
||||||
|
add: '添加组件'
|
||||||
|
},
|
||||||
|
viewport: {
|
||||||
|
title: '视口',
|
||||||
|
placeholder: '场景视口将显示在这里'
|
||||||
|
},
|
||||||
|
console: {
|
||||||
|
title: '控制台',
|
||||||
|
placeholder: '控制台输出将显示在这里'
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
plugins: '插件',
|
||||||
|
entities: '实体',
|
||||||
|
core: '核心',
|
||||||
|
active: '活跃',
|
||||||
|
inactive: '未激活'
|
||||||
|
}
|
||||||
|
};
|
||||||
143
packages/editor-core/src/Services/LocaleService.ts
Normal file
143
packages/editor-core/src/Services/LocaleService.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import type { IService } from '@esengine/ecs-framework';
|
||||||
|
import { Injectable } from '@esengine/ecs-framework';
|
||||||
|
import { createLogger } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
const logger = createLogger('LocaleService');
|
||||||
|
|
||||||
|
export type Locale = 'en' | 'zh';
|
||||||
|
|
||||||
|
export interface Translations {
|
||||||
|
[key: string]: string | Translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 国际化服务
|
||||||
|
*
|
||||||
|
* 管理编辑器的多语言支持
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class LocaleService implements IService {
|
||||||
|
private currentLocale: Locale = 'en';
|
||||||
|
private translations: Map<Locale, Translations> = new Map();
|
||||||
|
private changeListeners: Set<(locale: Locale) => void> = new Set();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const savedLocale = this.loadSavedLocale();
|
||||||
|
if (savedLocale) {
|
||||||
|
this.currentLocale = savedLocale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册语言包
|
||||||
|
*/
|
||||||
|
public registerTranslations(locale: Locale, translations: Translations): void {
|
||||||
|
this.translations.set(locale, translations);
|
||||||
|
logger.info(`Registered translations for locale: ${locale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前语言
|
||||||
|
*/
|
||||||
|
public getCurrentLocale(): Locale {
|
||||||
|
return this.currentLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前语言
|
||||||
|
*/
|
||||||
|
public setLocale(locale: Locale): void {
|
||||||
|
if (!this.translations.has(locale)) {
|
||||||
|
logger.warn(`Translations not found for locale: ${locale}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentLocale = locale;
|
||||||
|
this.saveLocale(locale);
|
||||||
|
|
||||||
|
this.changeListeners.forEach(listener => listener(locale));
|
||||||
|
|
||||||
|
logger.info(`Locale changed to: ${locale}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 翻译文本
|
||||||
|
*
|
||||||
|
* @param key - 翻译键,支持点分隔的路径如 "menu.file.save"
|
||||||
|
* @param fallback - 如果找不到翻译时的回退文本
|
||||||
|
*/
|
||||||
|
public t(key: string, fallback?: string): string {
|
||||||
|
const translations = this.translations.get(this.currentLocale);
|
||||||
|
if (!translations) {
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.getNestedValue(translations, key);
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback || key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听语言变化
|
||||||
|
*/
|
||||||
|
public onChange(listener: (locale: Locale) => void): () => void {
|
||||||
|
this.changeListeners.add(listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.changeListeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取嵌套对象的值
|
||||||
|
*/
|
||||||
|
private getNestedValue(obj: Translations, path: string): string | Translations | undefined {
|
||||||
|
const keys = path.split('.');
|
||||||
|
let current: any = obj;
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (current && typeof current === 'object' && key in current) {
|
||||||
|
current = current[key];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 localStorage 加载保存的语言设置
|
||||||
|
*/
|
||||||
|
private loadSavedLocale(): Locale | null {
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('editor-locale');
|
||||||
|
if (saved === 'en' || saved === 'zh') {
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to load saved locale:', error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存语言设置到 localStorage
|
||||||
|
*/
|
||||||
|
private saveLocale(locale: Locale): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('editor-locale', locale);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to save locale:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.translations.clear();
|
||||||
|
this.changeListeners.clear();
|
||||||
|
logger.info('LocaleService disposed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,5 +12,6 @@ export * from './Services/MessageHub';
|
|||||||
export * from './Services/SerializerRegistry';
|
export * from './Services/SerializerRegistry';
|
||||||
export * from './Services/EntityStoreService';
|
export * from './Services/EntityStoreService';
|
||||||
export * from './Services/ComponentRegistry';
|
export * from './Services/ComponentRegistry';
|
||||||
|
export * from './Services/LocaleService';
|
||||||
|
|
||||||
export * from './Types/UITypes';
|
export * from './Types/UITypes';
|
||||||
|
|||||||
Reference in New Issue
Block a user