diff --git a/GFramework.Game.Tests/GFramework.Game.Tests.csproj b/GFramework.Game.Tests/GFramework.Game.Tests.csproj index 6ad45dbe..9b6d0bdf 100644 --- a/GFramework.Game.Tests/GFramework.Game.Tests.csproj +++ b/GFramework.Game.Tests/GFramework.Game.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs b/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs new file mode 100644 index 00000000..2d651627 --- /dev/null +++ b/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs @@ -0,0 +1,74 @@ +using GFramework.Core.Abstractions.Localization; +using GFramework.Game.Abstractions.Setting; +using GFramework.Game.Abstractions.Setting.Data; +using GFramework.Godot.Setting; +using GFramework.Godot.Setting.Data; + +namespace GFramework.Game.Tests.Setting; + +/// +/// 覆盖 Godot 本地化设置应用器的语言同步行为,防止持久化语言仅影响 Godot 而未同步框架管理器。 +/// +[TestFixture] +public sealed class GodotLocalizationSettingsTests +{ + [Test] + public async Task Apply_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage() + { + var manager = new Mock(MockBehavior.Strict); + manager.Setup(it => it.SetLanguage("eng")); + string? appliedLocale = null; + + var applicator = CreateApplicator("English", manager.Object, locale => appliedLocale = locale); + + await applicator.Apply(); + + Assert.That(appliedLocale, Is.EqualTo("en")); + manager.Verify(it => it.SetLanguage("eng"), Times.Once); + } + + [Test] + public async Task Apply_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage() + { + var manager = new Mock(MockBehavior.Strict); + manager.Setup(it => it.SetLanguage("zhs")); + string? appliedLocale = null; + + var applicator = CreateApplicator("简体中文", manager.Object, locale => appliedLocale = locale); + + await applicator.Apply(); + + Assert.That(appliedLocale, Is.EqualTo("zh_CN")); + manager.Verify(it => it.SetLanguage("zhs"), Times.Once); + } + + [Test] + public async Task Apply_ShouldFallbackUnknownLanguageToEnglish() + { + var manager = new Mock(MockBehavior.Strict); + manager.Setup(it => it.SetLanguage("eng")); + string? appliedLocale = null; + + var applicator = CreateApplicator("Esperanto", manager.Object, locale => appliedLocale = locale); + + await applicator.Apply(); + + Assert.That(appliedLocale, Is.EqualTo("en")); + manager.Verify(it => it.SetLanguage("eng"), Times.Once); + } + + private static GodotLocalizationSettings CreateApplicator( + string language, + ILocalizationManager manager, + Action applyGodotLocale) + { + var settingsModel = new Mock(MockBehavior.Strict); + settingsModel.Setup(it => it.GetData()).Returns(new LocalizationSettings + { + Language = language + }); + + return new GodotLocalizationSettings(settingsModel.Object, new LocalizationMap(), () => manager, + applyGodotLocale); + } +} \ No newline at end of file diff --git a/GFramework.Godot/GlobalUsings.cs b/GFramework.Godot/GlobalUsings.cs index 4d271811..41d45db7 100644 --- a/GFramework.Godot/GlobalUsings.cs +++ b/GFramework.Godot/GlobalUsings.cs @@ -15,4 +15,5 @@ global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading; -global using System.Threading.Tasks; \ No newline at end of file +global using System.Threading.Tasks; +global using Godot; \ No newline at end of file diff --git a/GFramework.Godot/Setting/Data/LocalizationMap.cs b/GFramework.Godot/Setting/Data/LocalizationMap.cs index a5cface8..1864f4ae 100644 --- a/GFramework.Godot/Setting/Data/LocalizationMap.cs +++ b/GFramework.Godot/Setting/Data/LocalizationMap.cs @@ -18,12 +18,54 @@ namespace GFramework.Godot.Setting.Data; /// public class LocalizationMap { + private const string DefaultFrameworkLanguage = "eng"; + private const string DefaultGodotLocale = "en"; + /// - /// 用户语言 -> Godot locale 映射表 + /// 用户语言 -> Godot locale 映射表。 /// public Dictionary LanguageMap { get; set; } = new() { { "简体中文", "zh_CN" }, { "English", "en" } }; + + /// + /// 用户语言 -> GFramework 本地化语言码映射表。 + /// + public Dictionary FrameworkLanguageMap { get; set; } = new() + { + { "简体中文", "zhs" }, + { "English", "eng" } + }; + + /// + /// 解析用户保存的语言值对应的 Godot locale。 + /// + /// 设置系统中保存的语言值。 + /// 对应的 Godot locale;未知值时回退为英文。 + public string ResolveGodotLocale(string? storedLanguage) + { + if (string.IsNullOrWhiteSpace(storedLanguage)) + { + return DefaultGodotLocale; + } + + return LanguageMap.GetValueOrDefault(storedLanguage, DefaultGodotLocale); + } + + /// + /// 解析用户保存的语言值对应的框架语言码。 + /// + /// 设置系统中保存的语言值。 + /// 对应的框架语言码;未知值时回退为英文。 + public string ResolveFrameworkLanguage(string? storedLanguage) + { + if (string.IsNullOrWhiteSpace(storedLanguage)) + { + return DefaultFrameworkLanguage; + } + + return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage); + } } \ No newline at end of file diff --git a/GFramework.Godot/Setting/GodotLocalizationSettings.cs b/GFramework.Godot/Setting/GodotLocalizationSettings.cs index abbc9bfd..ddcfd6fc 100644 --- a/GFramework.Godot/Setting/GodotLocalizationSettings.cs +++ b/GFramework.Godot/Setting/GodotLocalizationSettings.cs @@ -11,32 +11,83 @@ // See the License for the specific language governing permissions and // limitations under the License. +using GFramework.Core.Abstractions.Localization; +using GFramework.Core.Architectures; using GFramework.Game.Abstractions.Setting; using GFramework.Game.Abstractions.Setting.Data; using GFramework.Godot.Setting.Data; -using Godot; namespace GFramework.Godot.Setting; /// -/// Godot本地化设置类,负责应用本地化配置到Godot引擎 +/// Godot 本地化设置类,负责将持久化语言配置同时应用到 Godot 引擎与 GFramework 本地化管理器。 /// -/// 设置模型 -/// 本地化映射表 -public class GodotLocalizationSettings(ISettingsModel model, LocalizationMap localizationMap) - : IResetApplyAbleSettings +public class GodotLocalizationSettings : IResetApplyAbleSettings { + private readonly Action _applyGodotLocale; + private readonly Func _localizationManagerResolver; + private readonly LocalizationMap _localizationMap; + private readonly ISettingsModel _model; + /// - /// 应用本地化设置到Godot引擎 + /// 初始化 Godot 本地化设置应用器,并默认从当前架构上下文解析框架本地化管理器。 + /// + /// 设置模型。 + /// 本地化映射表。 + public GodotLocalizationSettings(ISettingsModel model, LocalizationMap localizationMap) + : this(model, localizationMap, TryResolveLocalizationManager, TranslationServer.SetLocale) + { + } + + /// + /// 初始化 Godot 本地化设置应用器。 + /// + /// 设置模型。 + /// 本地化映射表。 + /// 框架本地化管理器解析器。 + public GodotLocalizationSettings( + ISettingsModel model, + LocalizationMap localizationMap, + Func localizationManagerResolver) + : this(model, localizationMap, localizationManagerResolver, TranslationServer.SetLocale) + { + } + + /// + /// 初始化 Godot 本地化设置应用器,并显式指定 Godot locale 应用动作。 + /// 该重载主要用于测试或自定义引擎桥接。 + /// + /// 设置模型。 + /// 本地化映射表。 + /// 框架本地化管理器解析器。 + /// Godot locale 应用动作。 + public GodotLocalizationSettings( + ISettingsModel model, + LocalizationMap localizationMap, + Func localizationManagerResolver, + Action applyGodotLocale) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _localizationMap = localizationMap ?? throw new ArgumentNullException(nameof(localizationMap)); + _localizationManagerResolver = + localizationManagerResolver ?? throw new ArgumentNullException(nameof(localizationManagerResolver)); + _applyGodotLocale = applyGodotLocale ?? throw new ArgumentNullException(nameof(applyGodotLocale)); + } + + /// + /// 应用本地化设置到 Godot 引擎与 GFramework 本地化管理器。 /// /// 完成的任务 public Task Apply() { - var settings = model.GetData(); - // 尝试从映射表获取 Godot locale - var locale = localizationMap.LanguageMap.GetValueOrDefault(settings.Language, "en"); - // 默认值 - TranslationServer.SetLocale(locale); + var settings = _model.GetData(); + var locale = _localizationMap.ResolveGodotLocale(settings.Language); + var frameworkLanguage = _localizationMap.ResolveFrameworkLanguage(settings.Language); + + _applyGodotLocale(locale); + + // 设置系统持久化的是用户可见语言值;这里需要同步框架语言码,避免 Godot 与框架状态分裂。 + _localizationManagerResolver()?.SetLanguage(frameworkLanguage); return Task.CompletedTask; } @@ -45,18 +96,30 @@ public class GodotLocalizationSettings(ISettingsModel model, LocalizationMap loc /// public void Reset() { - model.GetData().Reset(); + _model.GetData().Reset(); } /// /// 获取本地化设置的数据对象。 /// 该属性提供对本地化设置数据的只读访问。 /// - public ISettingsData Data { get; } = model.GetData(); + public ISettingsData Data => _model.GetData(); /// /// 获取本地化设置数据的类型。 /// 该属性返回本地化设置数据的具体类型信息。 /// public Type DataType { get; } = typeof(LocalizationSettings); + + private static ILocalizationManager? TryResolveLocalizationManager() + { + try + { + return GameContext.GetFirstArchitectureContext().GetSystem(); + } + catch (InvalidOperationException) + { + return null; + } + } } \ No newline at end of file diff --git a/docs/zh-CN/godot/setting.md b/docs/zh-CN/godot/setting.md index 385512ae..5a323626 100644 --- a/docs/zh-CN/godot/setting.md +++ b/docs/zh-CN/godot/setting.md @@ -3,7 +3,7 @@ ## 概述 Godot 设置模块是 GFramework.Godot 的核心组件之一,专门为 Godot 引擎提供游戏设置系统的实现。该模块将通用的设置框架与 Godot -引擎的特定功能相结合,提供了音频设置和图形设置的完整解决方案。 +引擎的特定功能相结合,提供了音频设置、图形设置和本地化设置的完整解决方案。 ## 核心类 @@ -61,23 +61,55 @@ Godot 图形设置实现类,继承自 GraphicsSettings 并实现 IApplyAbleSet - 窗口位置自动居中 - 多显示器支持 +### 本地化设置系统 + +#### LocalizationMap + +本地化映射配置类,用于把设置系统中保存的用户可见语言值解析为: + +- Godot `TranslationServer` 使用的 locale +- GFramework `ILocalizationManager` 使用的语言码 + +默认映射如下: + +- `"简体中文"` -> Godot `zh_CN`,框架语言码 `zhs` +- `"English"` -> Godot `en`,框架语言码 `eng` + +未知语言值会稳定回退到英文,避免重启后出现设置值与运行时语言状态不一致。 + +#### GodotLocalizationSettings + +Godot 本地化设置实现类,负责把 `LocalizationSettings` 同时应用到 Godot 引擎与 GFramework 本地化管理器。 + +**功能:** + +- 将语言设置应用到 `TranslationServer.SetLocale(...)` +- 同步 `ILocalizationManager.SetLanguage(...)` +- 通过统一映射避免 Godot locale 与框架语言码分裂 + ## 架构设计 ```mermaid graph TD A[AudioSettings] --> B[GodotAudioSettings] C[GraphicsSettings] --> D[GodotGraphicsSettings] - E[IApplyAbleSettings] --> B - E --> D + E[LocalizationSettings] --> F[GodotLocalizationSettings] + G[IApplyAbleSettings] --> B + G --> D + G --> F - G[AudioBusMap] --> B + H[AudioBusMap] --> B + I[LocalizationMap] --> F - B --> I[AudioServer API] - D --> J[DisplayServer API] + B --> J[AudioServer API] + D --> K[DisplayServer API] + F --> L[TranslationServer API] + F --> M[ILocalizationManager] - K[SettingsSystem] --> L[Apply Method] - L --> B - L --> D + N[SettingsSystem] --> O[Apply Method] + O --> B + O --> D + O --> F ``` ## 使用示例 @@ -568,4 +600,4 @@ var windowMode = DisplayServer.WindowGetMode(); GD.Print($"Screen: {screenSize}"); GD.Print($"Window: {windowSize} at {windowPos}"); GD.Print($"Mode: {windowMode}"); -``` \ No newline at end of file +```