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

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

View File

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

View File

@@ -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[];
}

View File

@@ -4,6 +4,9 @@
* Plugin-based editor framework for ECS Framework
*/
// Service Tokens | 服务令牌
export * from './tokens';
// 配置 | Configuration
export * from './Config';

View 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';