From e49713a84237a5f98c4359c465c1551413f830f6 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 18 Mar 2026 22:58:07 +0800
Subject: [PATCH 1/8] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E6=9C=AC?=
=?UTF-8?q?=E5=9C=B0=E5=8C=96=E7=B3=BB=E7=BB=9F=E6=94=AF=E6=8C=81=E5=A4=9A?=
=?UTF-8?q?=E8=AF=AD=E8=A8=80=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现 ILocalizationManager 接口及 LocalizationManager 管理器
- 添加 ILocalizationTable 和 ILocalizationString 接口及其实现
- 创建 LocalizationConfig 配置类用于管理本地化行为
- 实现 ConditionalFormatter 和 PluralFormatter 内置格式化器
- 添加本地化文档包括 API 参考和使用指南
- 集成本地化系统到核心框架架构中
---
.../Localization/ILocalizationFormatter.cs | 22 +
.../Localization/ILocalizationManager.cs | 89 ++++
.../Localization/ILocalizationString.cs | 50 ++
.../Localization/ILocalizationTable.cs | 48 ++
.../Localization/LocalizationConfig.cs | 37 ++
.../Localization/LocalizationException.cs | 31 ++
.../LocalizationKeyNotFoundException.cs | 29 ++
.../LocalizationTableNotFoundException.cs | 22 +
.../LocalizationIntegrationTests.cs | 105 ++++
.../Localization/LocalizationTableTests.cs | 84 +++
.../Formatters/ConditionalFormatter.cs | 38 ++
.../Formatters/PluralFormatter.cs | 43 ++
.../Localization/LocalizationManager.cs | 307 +++++++++++
.../Localization/LocalizationString.cs | 148 ++++++
.../Localization/LocalizationTable.cs | 136 +++++
docs/zh-CN/api-reference/index.md | 140 +++++
docs/zh-CN/core/index.md | 1 +
docs/zh-CN/core/localization.md | 487 ++++++++++++++++++
18 files changed, 1817 insertions(+)
create mode 100644 GFramework.Core.Abstractions/Localization/ILocalizationFormatter.cs
create mode 100644 GFramework.Core.Abstractions/Localization/ILocalizationManager.cs
create mode 100644 GFramework.Core.Abstractions/Localization/ILocalizationString.cs
create mode 100644 GFramework.Core.Abstractions/Localization/ILocalizationTable.cs
create mode 100644 GFramework.Core.Abstractions/Localization/LocalizationConfig.cs
create mode 100644 GFramework.Core.Abstractions/Localization/LocalizationException.cs
create mode 100644 GFramework.Core.Abstractions/Localization/LocalizationKeyNotFoundException.cs
create mode 100644 GFramework.Core.Abstractions/Localization/LocalizationTableNotFoundException.cs
create mode 100644 GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
create mode 100644 GFramework.Core.Tests/Localization/LocalizationTableTests.cs
create mode 100644 GFramework.Core/Localization/Formatters/ConditionalFormatter.cs
create mode 100644 GFramework.Core/Localization/Formatters/PluralFormatter.cs
create mode 100644 GFramework.Core/Localization/LocalizationManager.cs
create mode 100644 GFramework.Core/Localization/LocalizationString.cs
create mode 100644 GFramework.Core/Localization/LocalizationTable.cs
create mode 100644 docs/zh-CN/core/localization.md
diff --git a/GFramework.Core.Abstractions/Localization/ILocalizationFormatter.cs b/GFramework.Core.Abstractions/Localization/ILocalizationFormatter.cs
new file mode 100644
index 0000000..65faef8
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/ILocalizationFormatter.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化格式化器接口
+///
+public interface ILocalizationFormatter
+{
+ ///
+ /// 格式化器名称
+ ///
+ string Name { get; }
+
+ ///
+ /// 尝试格式化值
+ ///
+ /// 格式字符串
+ /// 要格式化的值
+ /// 格式提供者
+ /// 格式化结果
+ /// 是否成功格式化
+ bool TryFormat(string format, object value, IFormatProvider? provider, out string result);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/ILocalizationManager.cs b/GFramework.Core.Abstractions/Localization/ILocalizationManager.cs
new file mode 100644
index 0000000..835fd6d
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/ILocalizationManager.cs
@@ -0,0 +1,89 @@
+using System.Globalization;
+using GFramework.Core.Abstractions.Systems;
+
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化管理器接口
+///
+public interface ILocalizationManager : ISystem
+{
+ ///
+ /// 当前语言代码
+ ///
+ string CurrentLanguage { get; }
+
+ ///
+ /// 当前文化信息
+ ///
+ CultureInfo CurrentCulture { get; }
+
+ ///
+ /// 可用语言列表
+ ///
+ IReadOnlyList AvailableLanguages { get; }
+
+ ///
+ /// 设置当前语言
+ ///
+ /// 语言代码
+ void SetLanguage(string languageCode);
+
+ ///
+ /// 获取本地化表
+ ///
+ /// 表名
+ /// 本地化表
+ ILocalizationTable GetTable(string tableName);
+
+ ///
+ /// 获取本地化文本
+ ///
+ /// 表名
+ /// 键名
+ /// 本地化文本
+ string GetText(string table, string key);
+
+ ///
+ /// 获取本地化字符串(支持变量和格式化)
+ ///
+ /// 表名
+ /// 键名
+ /// 本地化字符串
+ ILocalizationString GetString(string table, string key);
+
+ ///
+ /// 尝试获取本地化文本
+ ///
+ /// 表名
+ /// 键名
+ /// 输出文本
+ /// 是否成功获取
+ bool TryGetText(string table, string key, out string text);
+
+ ///
+ /// 注册格式化器
+ ///
+ /// 格式化器名称
+ /// 格式化器实例
+ void RegisterFormatter(string name, ILocalizationFormatter formatter);
+
+ ///
+ /// 获取格式化器
+ ///
+ /// 格式化器名称
+ /// 格式化器实例,如果不存在则返回 null
+ ILocalizationFormatter? GetFormatter(string name);
+
+ ///
+ /// 订阅语言变化事件
+ ///
+ /// 回调函数
+ void SubscribeToLanguageChange(Action callback);
+
+ ///
+ /// 取消订阅语言变化事件
+ ///
+ /// 回调函数
+ void UnsubscribeFromLanguageChange(Action callback);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/ILocalizationString.cs b/GFramework.Core.Abstractions/Localization/ILocalizationString.cs
new file mode 100644
index 0000000..34146c4
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/ILocalizationString.cs
@@ -0,0 +1,50 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化字符串接口(支持变量和格式化)
+///
+public interface ILocalizationString
+{
+ ///
+ /// 表名
+ ///
+ string Table { get; }
+
+ ///
+ /// 键名
+ ///
+ string Key { get; }
+
+ ///
+ /// 添加变量
+ ///
+ /// 变量名
+ /// 变量值
+ /// 当前实例(支持链式调用)
+ ILocalizationString WithVariable(string name, object value);
+
+ ///
+ /// 批量添加变量
+ ///
+ /// 变量数组
+ /// 当前实例(支持链式调用)
+ ILocalizationString WithVariables(params (string name, object value)[] variables);
+
+ ///
+ /// 格式化并返回最终文本
+ ///
+ /// 格式化后的文本
+ string Format();
+
+ ///
+ /// 获取原始文本(不进行格式化)
+ ///
+ /// 原始文本
+ string GetRaw();
+
+ ///
+ /// 检查键是否存在
+ ///
+ /// 是否存在
+ bool Exists();
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/ILocalizationTable.cs b/GFramework.Core.Abstractions/Localization/ILocalizationTable.cs
new file mode 100644
index 0000000..29db256
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/ILocalizationTable.cs
@@ -0,0 +1,48 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化表接口
+///
+public interface ILocalizationTable
+{
+ ///
+ /// 表名
+ ///
+ string Name { get; }
+
+ ///
+ /// 语言代码
+ ///
+ string Language { get; }
+
+ ///
+ /// 回退表(当前表中找不到键时使用)
+ ///
+ ILocalizationTable? Fallback { get; }
+
+ ///
+ /// 获取原始文本(不进行格式化)
+ ///
+ /// 键名
+ /// 原始文本
+ string GetRawText(string key);
+
+ ///
+ /// 检查是否包含指定键
+ ///
+ /// 键名
+ /// 是否包含
+ bool ContainsKey(string key);
+
+ ///
+ /// 获取所有键
+ ///
+ /// 键集合
+ IEnumerable GetKeys();
+
+ ///
+ /// 合并覆盖数据
+ ///
+ /// 覆盖数据
+ void Merge(IReadOnlyDictionary overrides);
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/LocalizationConfig.cs b/GFramework.Core.Abstractions/Localization/LocalizationConfig.cs
new file mode 100644
index 0000000..26a3e1c
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/LocalizationConfig.cs
@@ -0,0 +1,37 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化配置
+///
+public class LocalizationConfig
+{
+ ///
+ /// 默认语言代码
+ ///
+ public string DefaultLanguage { get; set; } = "eng";
+
+ ///
+ /// 回退语言代码(当目标语言缺少键时使用)
+ ///
+ public string FallbackLanguage { get; set; } = "eng";
+
+ ///
+ /// 本地化文件路径(Godot 资源路径)
+ ///
+ public string LocalizationPath { get; set; } = "res://localization";
+
+ ///
+ /// 用户覆盖文件路径(用于热更新和自定义翻译)
+ ///
+ public string OverridePath { get; set; } = "user://localization_override";
+
+ ///
+ /// 是否启用热重载(监视覆盖文件变化)
+ ///
+ public bool EnableHotReload { get; set; } = true;
+
+ ///
+ /// 是否在加载时验证本地化文件
+ ///
+ public bool ValidateOnLoad { get; set; } = true;
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/LocalizationException.cs b/GFramework.Core.Abstractions/Localization/LocalizationException.cs
new file mode 100644
index 0000000..5f1345f
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/LocalizationException.cs
@@ -0,0 +1,31 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化异常基类
+///
+public class LocalizationException : Exception
+{
+ ///
+ /// 初始化本地化异常
+ ///
+ public LocalizationException()
+ {
+ }
+
+ ///
+ /// 初始化本地化异常
+ ///
+ /// 异常消息
+ public LocalizationException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// 初始化本地化异常
+ ///
+ /// 异常消息
+ /// 内部异常
+ public LocalizationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/LocalizationKeyNotFoundException.cs b/GFramework.Core.Abstractions/Localization/LocalizationKeyNotFoundException.cs
new file mode 100644
index 0000000..6611962
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/LocalizationKeyNotFoundException.cs
@@ -0,0 +1,29 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化键未找到异常
+///
+public class LocalizationKeyNotFoundException : LocalizationException
+{
+ ///
+ /// 初始化键未找到异常
+ ///
+ /// 表名
+ /// 键名
+ public LocalizationKeyNotFoundException(string tableName, string key)
+ : base($"Localization key '{key}' not found in table '{tableName}'")
+ {
+ TableName = tableName;
+ Key = key;
+ }
+
+ ///
+ /// 表名
+ ///
+ public string TableName { get; }
+
+ ///
+ /// 键名
+ ///
+ public string Key { get; }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Abstractions/Localization/LocalizationTableNotFoundException.cs b/GFramework.Core.Abstractions/Localization/LocalizationTableNotFoundException.cs
new file mode 100644
index 0000000..c6df214
--- /dev/null
+++ b/GFramework.Core.Abstractions/Localization/LocalizationTableNotFoundException.cs
@@ -0,0 +1,22 @@
+namespace GFramework.Core.Abstractions.Localization;
+
+///
+/// 本地化表未找到异常
+///
+public class LocalizationTableNotFoundException : LocalizationException
+{
+ ///
+ /// 初始化表未找到异常
+ ///
+ /// 表名
+ public LocalizationTableNotFoundException(string tableName)
+ : base($"Localization table '{tableName}' not found")
+ {
+ TableName = tableName;
+ }
+
+ ///
+ /// 表名
+ ///
+ public string TableName { get; }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs b/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
new file mode 100644
index 0000000..afce6a2
--- /dev/null
+++ b/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
@@ -0,0 +1,105 @@
+using GFramework.Core.Localization;
+
+namespace GFramework.Core.Tests.Localization;
+
+[TestFixture]
+public class LocalizationIntegrationTests
+{
+ private LocalizationManager? _manager;
+ private string _testDataPath = null!;
+
+ [SetUp]
+ public void Setup()
+ {
+ _testDataPath = "/tmp/localization_example";
+ var config = new LocalizationConfig
+ {
+ DefaultLanguage = "eng",
+ FallbackLanguage = "eng",
+ LocalizationPath = _testDataPath,
+ EnableHotReload = false,
+ ValidateOnLoad = false
+ };
+
+ _manager = new LocalizationManager(config);
+ _manager.Initialize();
+ }
+
+ [Test]
+ public void GetText_ShouldReturnEnglishText()
+ {
+ // Act
+ var title = _manager!.GetText("common", "game.title");
+
+ // Assert
+ Assert.That(title, Is.EqualTo("My Game"));
+ }
+
+ [Test]
+ public void GetString_WithVariable_ShouldFormatCorrectly()
+ {
+ // Act
+ var message = _manager!.GetString("common", "ui.message.welcome")
+ .WithVariable("playerName", "Alice")
+ .Format();
+
+ // Assert
+ Assert.That(message, Is.EqualTo("Welcome, Alice!"));
+ }
+
+ [Test]
+ public void SetLanguage_ShouldSwitchToChineseText()
+ {
+ // Act
+ _manager!.SetLanguage("zhs");
+ var title = _manager.GetText("common", "game.title");
+
+ // Assert
+ Assert.That(title, Is.EqualTo("我的游戏"));
+ }
+
+ [Test]
+ public void GetString_WithMultipleVariables_ShouldFormatCorrectly()
+ {
+ // Act
+ var health = _manager!.GetString("common", "status.health")
+ .WithVariable("current", 80)
+ .WithVariable("max", 100)
+ .Format();
+
+ // Assert
+ Assert.That(health, Is.EqualTo("Health: 80/100"));
+ }
+
+ [Test]
+ public void LanguageChange_ShouldTriggerCallback()
+ {
+ // Arrange
+ var callbackTriggered = false;
+ var newLanguage = string.Empty;
+
+ _manager!.SubscribeToLanguageChange(lang =>
+ {
+ callbackTriggered = true;
+ newLanguage = lang;
+ });
+
+ // Act
+ _manager.SetLanguage("zhs");
+
+ // Assert
+ Assert.That(callbackTriggered, Is.True);
+ Assert.That(newLanguage, Is.EqualTo("zhs"));
+ }
+
+ [Test]
+ public void AvailableLanguages_ShouldContainBothLanguages()
+ {
+ // Act
+ var languages = _manager!.AvailableLanguages;
+
+ // Assert
+ Assert.That(languages, Contains.Item("eng"));
+ Assert.That(languages, Contains.Item("zhs"));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core.Tests/Localization/LocalizationTableTests.cs b/GFramework.Core.Tests/Localization/LocalizationTableTests.cs
new file mode 100644
index 0000000..d86b775
--- /dev/null
+++ b/GFramework.Core.Tests/Localization/LocalizationTableTests.cs
@@ -0,0 +1,84 @@
+using GFramework.Core.Localization;
+
+namespace GFramework.Core.Tests.Localization;
+
+[TestFixture]
+public class LocalizationTableTests
+{
+ [Test]
+ public void GetRawText_ShouldReturnCorrectText()
+ {
+ // Arrange
+ var data = new Dictionary
+ {
+ ["test.key"] = "Test Value"
+ };
+ var table = new LocalizationTable("test", "eng", data);
+
+ // Act
+ var result = table.GetRawText("test.key");
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Test Value"));
+ }
+
+ [Test]
+ public void GetRawText_WithFallback_ShouldReturnFallbackValue()
+ {
+ // Arrange
+ var fallbackData = new Dictionary
+ {
+ ["test.key"] = "Fallback Value"
+ };
+ var fallbackTable = new LocalizationTable("test", "eng", fallbackData);
+
+ var data = new Dictionary();
+ var table = new LocalizationTable("test", "zhs", data, fallbackTable);
+
+ // Act
+ var result = table.GetRawText("test.key");
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Fallback Value"));
+ }
+
+ [Test]
+ public void ContainsKey_ShouldReturnTrue_WhenKeyExists()
+ {
+ // Arrange
+ var data = new Dictionary
+ {
+ ["test.key"] = "Test Value"
+ };
+ var table = new LocalizationTable("test", "eng", data);
+
+ // Act
+ var result = table.ContainsKey("test.key");
+
+ // Assert
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void Merge_ShouldOverrideExistingValues()
+ {
+ // Arrange
+ var data = new Dictionary
+ {
+ ["test.key"] = "Original Value"
+ };
+ var table = new LocalizationTable("test", "eng", data);
+
+ var overrides = new Dictionary
+ {
+ ["test.key"] = "Override Value"
+ };
+
+ // Act
+ table.Merge(overrides);
+ var result = table.GetRawText("test.key");
+
+ // Assert
+ Assert.That(result, Is.EqualTo("Override Value"));
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Localization/Formatters/ConditionalFormatter.cs b/GFramework.Core/Localization/Formatters/ConditionalFormatter.cs
new file mode 100644
index 0000000..03172aa
--- /dev/null
+++ b/GFramework.Core/Localization/Formatters/ConditionalFormatter.cs
@@ -0,0 +1,38 @@
+using GFramework.Core.Abstractions.Localization;
+
+namespace GFramework.Core.Localization.Formatters;
+
+///
+/// 条件格式化器
+/// 格式: {condition:if:trueText|falseText}
+/// 示例: {upgraded:if:Upgraded|Normal}
+///
+public class ConditionalFormatter : ILocalizationFormatter
+{
+ ///
+ public string Name => "if";
+
+ ///
+ public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
+ {
+ result = string.Empty;
+
+ try
+ {
+ var parts = format.Split('|');
+
+ if (parts.Length != 2)
+ {
+ return false;
+ }
+
+ var condition = value is bool b ? b : Convert.ToBoolean(value);
+ result = condition ? parts[0] : parts[1];
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Localization/Formatters/PluralFormatter.cs b/GFramework.Core/Localization/Formatters/PluralFormatter.cs
new file mode 100644
index 0000000..e1ee7da
--- /dev/null
+++ b/GFramework.Core/Localization/Formatters/PluralFormatter.cs
@@ -0,0 +1,43 @@
+using GFramework.Core.Abstractions.Localization;
+
+namespace GFramework.Core.Localization.Formatters;
+
+///
+/// 复数格式化器
+/// 格式: {count:plural:singular|plural}
+/// 示例: {count:plural:item|items}
+///
+public class PluralFormatter : ILocalizationFormatter
+{
+ ///
+ public string Name => "plural";
+
+ ///
+ public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
+ {
+ result = string.Empty;
+
+ if (value is not IConvertible convertible)
+ {
+ return false;
+ }
+
+ try
+ {
+ var number = convertible.ToDecimal(provider);
+ var parts = format.Split('|');
+
+ if (parts.Length != 2)
+ {
+ return false;
+ }
+
+ result = Math.Abs(number) == 1 ? parts[0] : parts[1];
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Localization/LocalizationManager.cs b/GFramework.Core/Localization/LocalizationManager.cs
new file mode 100644
index 0000000..202bbcd
--- /dev/null
+++ b/GFramework.Core/Localization/LocalizationManager.cs
@@ -0,0 +1,307 @@
+using System.Globalization;
+using System.IO;
+using System.Text.Json;
+using GFramework.Core.Abstractions.Localization;
+using GFramework.Core.Systems;
+
+namespace GFramework.Core.Localization;
+
+///
+/// 本地化管理器实现
+///
+public class LocalizationManager : AbstractSystem, ILocalizationManager
+{
+ private readonly LocalizationConfig _config;
+ private readonly Dictionary _formatters;
+ private readonly List> _languageChangeCallbacks;
+ private readonly Dictionary> _tables;
+ private List _availableLanguages;
+ private CultureInfo _currentCulture;
+ private string _currentLanguage;
+
+ ///
+ /// 初始化本地化管理器
+ ///
+ /// 配置
+ public LocalizationManager(LocalizationConfig? config = null)
+ {
+ _config = config ?? new LocalizationConfig();
+ _tables = new Dictionary>();
+ _formatters = new Dictionary();
+ _languageChangeCallbacks = new List>();
+ _currentLanguage = _config.DefaultLanguage;
+ _currentCulture = GetCultureInfo(_currentLanguage);
+ _availableLanguages = new List();
+ }
+
+ ///
+ public string CurrentLanguage => _currentLanguage;
+
+ ///
+ public CultureInfo CurrentCulture => _currentCulture;
+
+ ///
+ public IReadOnlyList AvailableLanguages => _availableLanguages;
+
+ ///
+ public void SetLanguage(string languageCode)
+ {
+ if (string.IsNullOrEmpty(languageCode))
+ {
+ throw new ArgumentNullException(nameof(languageCode));
+ }
+
+ if (_currentLanguage == languageCode)
+ {
+ return;
+ }
+
+ LoadLanguage(languageCode);
+ _currentLanguage = languageCode;
+ _currentCulture = GetCultureInfo(languageCode);
+
+ // 触发语言变化回调
+ TriggerLanguageChange();
+ }
+
+ ///
+ public ILocalizationTable GetTable(string tableName)
+ {
+ if (string.IsNullOrEmpty(tableName))
+ {
+ throw new ArgumentNullException(nameof(tableName));
+ }
+
+ if (!_tables.TryGetValue(_currentLanguage, out var languageTables))
+ {
+ throw new LocalizationTableNotFoundException(tableName);
+ }
+
+ if (!languageTables.TryGetValue(tableName, out var table))
+ {
+ throw new LocalizationTableNotFoundException(tableName);
+ }
+
+ return table;
+ }
+
+ ///
+ public string GetText(string table, string key)
+ {
+ return GetTable(table).GetRawText(key);
+ }
+
+ ///
+ public ILocalizationString GetString(string table, string key)
+ {
+ return new LocalizationString(this, table, key);
+ }
+
+ ///
+ public bool TryGetText(string table, string key, out string text)
+ {
+ try
+ {
+ text = GetText(table, key);
+ return true;
+ }
+ catch
+ {
+ text = string.Empty;
+ return false;
+ }
+ }
+
+ ///
+ public void RegisterFormatter(string name, ILocalizationFormatter formatter)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ _formatters[name] = formatter ?? throw new ArgumentNullException(nameof(formatter));
+ }
+
+ ///
+ public ILocalizationFormatter? GetFormatter(string name)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return null;
+ }
+
+ return _formatters.TryGetValue(name, out var formatter) ? formatter : null;
+ }
+
+ ///
+ public void SubscribeToLanguageChange(Action callback)
+ {
+ if (callback == null)
+ {
+ throw new ArgumentNullException(nameof(callback));
+ }
+
+ if (!_languageChangeCallbacks.Contains(callback))
+ {
+ _languageChangeCallbacks.Add(callback);
+ }
+ }
+
+ ///
+ public void UnsubscribeFromLanguageChange(Action callback)
+ {
+ if (callback == null)
+ {
+ throw new ArgumentNullException(nameof(callback));
+ }
+
+ _languageChangeCallbacks.Remove(callback);
+ }
+
+ ///
+ protected override void OnInit()
+ {
+ // 扫描可用语言
+ ScanAvailableLanguages();
+
+ // 加载默认语言
+ LoadLanguage(_config.DefaultLanguage);
+ }
+
+ ///
+ protected override void OnDestroy()
+ {
+ _tables.Clear();
+ _formatters.Clear();
+ _languageChangeCallbacks.Clear();
+ }
+
+ ///
+ /// 扫描可用语言
+ ///
+ private void ScanAvailableLanguages()
+ {
+ _availableLanguages.Clear();
+
+ var localizationPath = _config.LocalizationPath;
+ if (!Directory.Exists(localizationPath))
+ {
+ _availableLanguages.Add(_config.DefaultLanguage);
+ return;
+ }
+
+ var directories = Directory.GetDirectories(localizationPath);
+ foreach (var dir in directories)
+ {
+ var languageCode = Path.GetFileName(dir);
+ if (!string.IsNullOrEmpty(languageCode))
+ {
+ _availableLanguages.Add(languageCode);
+ }
+ }
+
+ if (_availableLanguages.Count == 0)
+ {
+ _availableLanguages.Add(_config.DefaultLanguage);
+ }
+ }
+
+ ///
+ /// 加载语言
+ ///
+ private void LoadLanguage(string languageCode)
+ {
+ if (_tables.ContainsKey(languageCode))
+ {
+ return; // 已加载
+ }
+
+ var languageTables = new Dictionary();
+
+ // 加载回退语言(如果不是默认语言)
+ Dictionary? fallbackTables = null;
+ if (languageCode != _config.FallbackLanguage)
+ {
+ LoadLanguage(_config.FallbackLanguage);
+ _tables.TryGetValue(_config.FallbackLanguage, out fallbackTables);
+ }
+
+ // 加载目标语言
+ var languagePath = Path.Combine(_config.LocalizationPath, languageCode);
+ if (Directory.Exists(languagePath))
+ {
+ var jsonFiles = Directory.GetFiles(languagePath, "*.json");
+ foreach (var file in jsonFiles)
+ {
+ var tableName = Path.GetFileNameWithoutExtension(file);
+ var data = LoadJsonFile(file);
+
+ ILocalizationTable? fallback = null;
+ fallbackTables?.TryGetValue(tableName, out fallback);
+
+ languageTables[tableName] = new LocalizationTable(tableName, languageCode, data, fallback);
+ }
+ }
+
+ _tables[languageCode] = languageTables;
+ }
+
+ ///
+ /// 加载 JSON 文件
+ ///
+ private static Dictionary LoadJsonFile(string filePath)
+ {
+ var json = File.ReadAllText(filePath);
+ var data = JsonSerializer.Deserialize>(json);
+ return data ?? new Dictionary();
+ }
+
+ ///
+ /// 获取文化信息
+ ///
+ private static CultureInfo GetCultureInfo(string languageCode)
+ {
+ try
+ {
+ // 尝试映射常见的语言代码
+ var cultureCode = languageCode switch
+ {
+ "eng" => "en-US",
+ "zhs" => "zh-CN",
+ "zht" => "zh-TW",
+ "jpn" => "ja-JP",
+ "kor" => "ko-KR",
+ "fra" => "fr-FR",
+ "deu" => "de-DE",
+ "spa" => "es-ES",
+ "rus" => "ru-RU",
+ _ => languageCode
+ };
+
+ return new CultureInfo(cultureCode);
+ }
+ catch
+ {
+ return CultureInfo.InvariantCulture;
+ }
+ }
+
+ ///
+ /// 触发语言变化事件
+ ///
+ private void TriggerLanguageChange()
+ {
+ foreach (var callback in _languageChangeCallbacks.ToList())
+ {
+ try
+ {
+ callback(_currentLanguage);
+ }
+ catch
+ {
+ // 忽略回调异常
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs
new file mode 100644
index 0000000..e15a5c1
--- /dev/null
+++ b/GFramework.Core/Localization/LocalizationString.cs
@@ -0,0 +1,148 @@
+using System.Text.RegularExpressions;
+using GFramework.Core.Abstractions.Localization;
+
+namespace GFramework.Core.Localization;
+
+///
+/// 本地化字符串实现
+///
+public class LocalizationString : ILocalizationString
+{
+ private readonly ILocalizationManager _manager;
+ private readonly Dictionary _variables;
+
+ ///
+ /// 初始化本地化字符串
+ ///
+ /// 本地化管理器
+ /// 表名
+ /// 键名
+ public LocalizationString(ILocalizationManager manager, string table, string key)
+ {
+ _manager = manager ?? throw new ArgumentNullException(nameof(manager));
+ Table = table ?? throw new ArgumentNullException(nameof(table));
+ Key = key ?? throw new ArgumentNullException(nameof(key));
+ _variables = new Dictionary();
+ }
+
+ ///
+ public string Table { get; }
+
+ ///
+ public string Key { get; }
+
+ ///
+ public ILocalizationString WithVariable(string name, object value)
+ {
+ if (name == null)
+ {
+ throw new ArgumentNullException(nameof(name));
+ }
+
+ _variables[name] = value;
+ return this;
+ }
+
+ ///
+ public ILocalizationString WithVariables(params (string name, object value)[] variables)
+ {
+ if (variables == null)
+ {
+ throw new ArgumentNullException(nameof(variables));
+ }
+
+ foreach (var (name, value) in variables)
+ {
+ WithVariable(name, value);
+ }
+
+ return this;
+ }
+
+ ///
+ public string Format()
+ {
+ var rawText = GetRaw();
+ return FormatString(rawText, _variables, _manager);
+ }
+
+ ///
+ public string GetRaw()
+ {
+ if (!_manager.TryGetText(Table, Key, out var text))
+ {
+ return $"[{Table}.{Key}]";
+ }
+
+ return text;
+ }
+
+ ///
+ public bool Exists()
+ {
+ return _manager.TryGetText(Table, Key, out _);
+ }
+
+ ///
+ /// 格式化字符串(支持变量替换和格式化器)
+ ///
+ private static string FormatString(
+ string template,
+ Dictionary variables,
+ ILocalizationManager manager)
+ {
+ if (string.IsNullOrEmpty(template))
+ {
+ return template;
+ }
+
+ // 匹配 {variableName} 或 {variableName:formatter:args}
+ var pattern = @"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}";
+ var regex = new Regex(pattern);
+
+ return regex.Replace(template, match =>
+ {
+ var variableName = match.Groups[1].Value;
+ var formatterName = match.Groups[2].Success ? match.Groups[2].Value : null;
+ var formatterArgs = match.Groups[3].Success ? match.Groups[3].Value : null;
+
+ if (!variables.TryGetValue(variableName, out var value))
+ {
+ return match.Value; // 保持原样
+ }
+
+ // 如果没有格式化器,直接转换为字符串
+ if (string.IsNullOrEmpty(formatterName))
+ {
+ return value switch
+ {
+ IFormattable formattable => formattable.ToString(null, manager.CurrentCulture),
+ _ => value?.ToString() ?? string.Empty
+ };
+ }
+
+ // 尝试使用注册的格式化器
+ var formatter = GetFormatter(manager, formatterName);
+ if (formatter != null && formatter.TryFormat(formatterArgs ?? string.Empty, value, manager.CurrentCulture,
+ out var result))
+ {
+ return result;
+ }
+
+ // 格式化失败,返回基本格式化
+ return value switch
+ {
+ IFormattable formattable => formattable.ToString(null, manager.CurrentCulture),
+ _ => value?.ToString() ?? string.Empty
+ };
+ });
+ }
+
+ ///
+ /// 获取格式化器
+ ///
+ private static ILocalizationFormatter? GetFormatter(ILocalizationManager manager, string name)
+ {
+ return manager.GetFormatter(name);
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Core/Localization/LocalizationTable.cs b/GFramework.Core/Localization/LocalizationTable.cs
new file mode 100644
index 0000000..2a063f2
--- /dev/null
+++ b/GFramework.Core/Localization/LocalizationTable.cs
@@ -0,0 +1,136 @@
+using GFramework.Core.Abstractions.Localization;
+
+namespace GFramework.Core.Localization;
+
+///
+/// 本地化表实现
+///
+public class LocalizationTable : ILocalizationTable
+{
+ ///
+ /// 存储原始本地化数据的字典
+ ///
+ private readonly Dictionary _data;
+
+ ///
+ /// 存储覆盖数据的字典,优先级高于原始数据
+ ///
+ private readonly Dictionary _overrides;
+
+ ///
+ /// 初始化本地化表
+ ///
+ /// 表名
+ /// 语言代码
+ /// 数据字典
+ /// 回退表
+ public LocalizationTable(
+ string name,
+ string language,
+ IReadOnlyDictionary data,
+ ILocalizationTable? fallback = null)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ Language = language ?? throw new ArgumentNullException(nameof(language));
+ _data = new Dictionary(data);
+ _overrides = new Dictionary();
+ Fallback = fallback;
+ }
+
+ ///
+ /// 获取本地化表的名称
+ ///
+ public string Name { get; }
+
+ ///
+ /// 获取语言代码
+ ///
+ public string Language { get; }
+
+ ///
+ /// 获取回退表,当当前表找不到键时用于查找
+ ///
+ public ILocalizationTable? Fallback { get; }
+
+ ///
+ /// 获取指定键的原始文本内容
+ ///
+ /// 要查找的本地化键
+ /// 找到的本地化文本值
+ /// 当 key 为 null 时抛出
+ /// 当键在表中不存在且无回退表时抛出
+ public string GetRawText(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ // 优先使用覆盖数据
+ if (_overrides.TryGetValue(key, out var overrideValue))
+ {
+ return overrideValue;
+ }
+
+ // 然后使用原始数据
+ if (_data.TryGetValue(key, out var value))
+ {
+ return value;
+ }
+
+ // 最后尝试回退表
+ if (Fallback != null && Fallback.ContainsKey(key))
+ {
+ return Fallback.GetRawText(key);
+ }
+
+ throw new LocalizationKeyNotFoundException(Name, key);
+ }
+
+ ///
+ /// 检查是否包含指定的键
+ ///
+ /// 要检查的本地化键
+ /// 如果存在则返回 true,否则返回 false
+ public bool ContainsKey(string key)
+ {
+ return _overrides.ContainsKey(key)
+ || _data.ContainsKey(key)
+ || (Fallback?.ContainsKey(key) ?? false);
+ }
+
+ ///
+ /// 获取所有可用的本地化键集合
+ ///
+ /// 包含所有键的可枚举集合
+ public IEnumerable GetKeys()
+ {
+ var keys = new HashSet(_data.Keys);
+ keys.UnionWith(_overrides.Keys);
+
+ if (Fallback != null)
+ {
+ keys.UnionWith(Fallback.GetKeys());
+ }
+
+ return keys;
+ }
+
+ ///
+ /// 合并覆盖数据到当前表
+ ///
+ /// 要合并的覆盖数据字典
+ /// 当 overrides 为 null 时抛出
+ public void Merge(IReadOnlyDictionary overrides)
+ {
+ if (overrides == null)
+ {
+ throw new ArgumentNullException(nameof(overrides));
+ }
+
+ foreach (var (key, value) in overrides)
+ {
+ _overrides[key] = value;
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/zh-CN/api-reference/index.md b/docs/zh-CN/api-reference/index.md
index d3a9748..18b3ec3 100644
--- a/docs/zh-CN/api-reference/index.md
+++ b/docs/zh-CN/api-reference/index.md
@@ -66,6 +66,23 @@ IoC 容器命名空间。
| `IObjectPool` | 对象池接口 |
| `ObjectPool` | 对象池实现 |
+### GFramework.Core.Localization
+
+本地化系统命名空间。
+
+#### 主要类型
+
+| 类型 | 说明 |
+|--------------------------|----------|
+| `ILocalizationManager` | 本地化管理器接口 |
+| `ILocalizationTable` | 本地化表接口 |
+| `ILocalizationString` | 本地化字符串接口 |
+| `ILocalizationFormatter` | 格式化器接口 |
+| `LocalizationConfig` | 本地化配置类 |
+| `LocalizationManager` | 本地化管理器实现 |
+| `LocalizationTable` | 本地化表实现 |
+| `LocalizationString` | 本地化字符串实现 |
+
## 常用 API
### Architecture
@@ -247,6 +264,99 @@ public class BindableProperty
}
```
+### ILocalizationManager
+
+```csharp
+public interface ILocalizationManager : ISystem
+{
+ // 获取当前语言代码
+ string CurrentLanguage { get; }
+
+ // 获取当前文化信息
+ CultureInfo CurrentCulture { get; }
+
+ // 获取可用语言列表
+ IReadOnlyList AvailableLanguages { get; }
+
+ // 设置当前语言
+ void SetLanguage(string languageCode);
+
+ // 获取本地化表
+ ILocalizationTable GetTable(string tableName);
+
+ // 获取本地化文本
+ string GetText(string table, string key);
+
+ // 获取本地化字符串(支持变量)
+ ILocalizationString GetString(string table, string key);
+
+ // 尝试获取本地化文本
+ bool TryGetText(string table, string key, out string text);
+
+ // 注册格式化器
+ void RegisterFormatter(string name, ILocalizationFormatter formatter);
+
+ // 订阅语言变化事件
+ void SubscribeToLanguageChange(Action callback);
+
+ // 取消订阅语言变化事件
+ void UnsubscribeFromLanguageChange(Action callback);
+}
+```
+
+### ILocalizationString
+
+```csharp
+public interface ILocalizationString
+{
+ // 获取表名
+ string Table { get; }
+
+ // 获取键名
+ string Key { get; }
+
+ // 添加变量
+ ILocalizationString WithVariable(string name, object value);
+
+ // 批量添加变量
+ ILocalizationString WithVariables(params (string name, object value)[] variables);
+
+ // 格式化并返回文本
+ string Format();
+
+ // 获取原始文本
+ string GetRaw();
+
+ // 检查键是否存在
+ bool Exists();
+}
+```
+
+### LocalizationConfig
+
+```csharp
+public class LocalizationConfig
+{
+ // 默认语言代码
+ public string DefaultLanguage { get; set; } = "eng";
+
+ // 回退语言代码
+ public string FallbackLanguage { get; set; } = "eng";
+
+ // 本地化文件路径
+ public string LocalizationPath { get; set; } = "res://localization";
+
+ // 用户覆盖路径
+ public string OverridePath { get; set; } = "user://localization_override";
+
+ // 是否启用热重载
+ public bool EnableHotReload { get; set; } = true;
+
+ // 是否在加载时验证
+ public bool ValidateOnLoad { get; set; } = true;
+}
+```
+
## 扩展方法
### 架构扩展
@@ -406,6 +516,36 @@ public class PlayerSystem : AbstractSystem
}
```
+### 使用本地化
+
+```csharp
+// 初始化本地化管理器
+var config = new LocalizationConfig
+{
+ DefaultLanguage = "eng",
+ LocalizationPath = "res://localization"
+};
+var locManager = new LocalizationManager(config);
+locManager.Initialize();
+
+// 获取简单文本
+string title = locManager.GetText("common", "game.title");
+
+// 使用变量
+var message = locManager.GetString("common", "ui.message.welcome")
+ .WithVariable("playerName", "Alice")
+ .Format();
+
+// 切换语言
+locManager.SetLanguage("zhs");
+
+// 监听语言变化
+locManager.SubscribeToLanguageChange(language =>
+{
+ Console.WriteLine($"Language changed to: {language}");
+});
+```
+
---
更多详情请查看各模块的详细文档。
diff --git a/docs/zh-CN/core/index.md b/docs/zh-CN/core/index.md
index 189f0a6..fb9602b 100644
--- a/docs/zh-CN/core/index.md
+++ b/docs/zh-CN/core/index.md
@@ -421,6 +421,7 @@ public class PlayerController : IController
| **extensions** | 扩展方法,简化 API 调用 | [查看](./extensions) |
| **logging** | 日志系统,记录运行日志 | [查看](./logging) |
| **environment** | 环境接口,提供运行环境信息 | [查看](./environment) |
+| **localization** | 本地化系统,多语言支持 | [查看](./localization) |
## 组件联动
diff --git a/docs/zh-CN/core/localization.md b/docs/zh-CN/core/localization.md
new file mode 100644
index 0000000..440e051
--- /dev/null
+++ b/docs/zh-CN/core/localization.md
@@ -0,0 +1,487 @@
+# Localization 本地化系统
+
+## 概述
+
+Localization 包提供了完整的多语言本地化支持,实现了游戏文本的国际化管理。通过本地化系统,可以轻松实现多语言切换、动态变量替换、回退机制等功能。
+
+本地化系统是 GFramework 架构中的 System 层组件,与其他系统无缝集成,支持类型安全的 API 和流畅的使用体验。
+
+## 核心接口
+
+### ILocalizationManager
+
+本地化管理器接口,继承自 `ISystem`,提供本地化的核心功能。
+
+**核心属性:**
+
+```csharp
+string CurrentLanguage { get; } // 当前语言代码
+CultureInfo CurrentCulture { get; } // 当前文化信息
+IReadOnlyList AvailableLanguages { get; } // 可用语言列表
+```
+
+**核心方法:**
+
+```csharp
+void SetLanguage(string languageCode); // 设置当前语言
+ILocalizationTable GetTable(string tableName); // 获取本地化表
+string GetText(string table, string key); // 获取本地化文本
+ILocalizationString GetString(string table, string key); // 获取本地化字符串(支持变量)
+bool TryGetText(string table, string key, out string text); // 尝试获取文本
+void RegisterFormatter(string name, ILocalizationFormatter formatter); // 注册格式化器
+void SubscribeToLanguageChange(Action callback); // 订阅语言变化
+void UnsubscribeFromLanguageChange(Action callback); // 取消订阅
+```
+
+### ILocalizationTable
+
+本地化表接口,表示单个语言的本地化数据表。
+
+**核心属性:**
+
+```csharp
+string Name { get; } // 表名
+string Language { get; } // 语言代码
+ILocalizationTable? Fallback { get; } // 回退表
+```
+
+**核心方法:**
+
+```csharp
+string GetRawText(string key); // 获取原始文本
+bool ContainsKey(string key); // 检查键是否存在
+IEnumerable GetKeys(); // 获取所有键
+void Merge(IReadOnlyDictionary overrides); // 合并覆盖数据
+```
+
+### ILocalizationString
+
+本地化字符串接口,支持变量替换和格式化。
+
+**核心属性:**
+
+```csharp
+string Table { get; } // 表名
+string Key { get; } // 键名
+```
+
+**核心方法:**
+
+```csharp
+ILocalizationString WithVariable(string name, object value); // 添加变量
+ILocalizationString WithVariables(params (string name, object value)[] variables); // 批量添加变量
+string Format(); // 格式化并返回文本
+string GetRaw(); // 获取原始文本
+bool Exists(); // 检查键是否存在
+```
+
+### ILocalizationFormatter
+
+格式化器接口,用于自定义变量格式化逻辑。
+
+**核心属性:**
+
+```csharp
+string Name { get; } // 格式化器名称
+```
+
+**核心方法:**
+
+```csharp
+bool TryFormat(string format, object value, IFormatProvider? provider, out string result);
+```
+
+## 配置类
+
+### LocalizationConfig
+
+本地化配置类,用于配置本地化系统的行为。
+
+**配置属性:**
+
+```csharp
+string DefaultLanguage { get; set; } // 默认语言代码,默认 "eng"
+string FallbackLanguage { get; set; } // 回退语言代码,默认 "eng"
+string LocalizationPath { get; set; } // 本地化文件路径,默认 "res://localization"
+string OverridePath { get; set; } // 用户覆盖路径,默认 "user://localization_override"
+bool EnableHotReload { get; set; } // 是否启用热重载,默认 true
+bool ValidateOnLoad { get; set; } // 是否在加载时验证,默认 true
+```
+
+## 文件组织
+
+### 目录结构
+
+```
+res://localization/
+├── eng/ # 英文
+│ ├── common.json # 通用文本
+│ ├── ui.json # UI 文本
+│ ├── cards.json # 卡牌文本
+│ └── ...
+├── zhs/ # 简体中文
+│ ├── common.json
+│ ├── ui.json
+│ └── ...
+└── ...
+
+user://localization_override/ # 用户覆盖(可选)
+├── eng/
+└── zhs/
+```
+
+### JSON 文件格式
+
+```json
+{
+ "game.title": "My Game",
+ "game.version": "Version {version}",
+ "ui.button.start": "Start Game",
+ "ui.message.welcome": "Welcome, {playerName}!",
+ "combat.damage": "Deal {damage} damage",
+ "status.health": "Health: {current}/{max}"
+}
+```
+
+**命名约定:**
+
+- 使用点号分隔的层级结构(如 `ui.button.start`)
+- 变量使用花括号包裹(如 `{playerName}`)
+- 键名使用小写字母和点号
+
+## 基本使用
+
+### 初始化本地化管理器
+
+```csharp
+using GFramework.Core.Abstractions.Localization;
+using GFramework.Core.Localization;
+
+// 创建配置
+var config = new LocalizationConfig
+{
+ DefaultLanguage = "eng",
+ FallbackLanguage = "eng",
+ LocalizationPath = "res://localization"
+};
+
+// 创建管理器
+var locManager = new LocalizationManager(config);
+locManager.Initialize();
+```
+
+### 在 Architecture 中注册
+
+```csharp
+public class GameArchitecture : Architecture
+{
+ protected override void OnInit()
+ {
+ // 注册本地化管理器
+ this.RegisterSystem(new LocalizationManager());
+ }
+}
+```
+
+### 获取本地化文本
+
+```csharp
+// 获取管理器
+var locManager = this.GetSystem();
+
+// 简单文本
+string title = locManager.GetText("common", "game.title");
+// 结果: "My Game"
+
+// 安全获取
+if (locManager.TryGetText("common", "game.title", out var text))
+{
+ Debug.Log(text);
+}
+```
+
+### 使用变量
+
+```csharp
+// 单个变量
+var message = locManager.GetString("common", "ui.message.welcome")
+ .WithVariable("playerName", "Alice")
+ .Format();
+// 结果: "Welcome, Alice!"
+
+// 多个变量
+var health = locManager.GetString("common", "status.health")
+ .WithVariable("current", 80)
+ .WithVariable("max", 100)
+ .Format();
+// 结果: "Health: 80/100"
+
+// 链式调用
+var text = locManager.GetString("common", "game.version")
+ .WithVariable("version", "1.0.0")
+ .Format();
+// 结果: "Version 1.0.0"
+```
+
+### 切换语言
+
+```csharp
+// 切换到简体中文
+locManager.SetLanguage("zhs");
+
+// 获取文本(自动使用新语言)
+string title = locManager.GetText("common", "game.title");
+// 结果: "我的游戏"
+
+// 获取当前语言
+string currentLang = locManager.CurrentLanguage; // "zhs"
+
+// 获取可用语言列表
+var languages = locManager.AvailableLanguages;
+foreach (var lang in languages)
+{
+ Debug.Log(lang); // "eng", "zhs", ...
+}
+```
+
+### 监听语言变化
+
+```csharp
+// 订阅语言变化事件
+locManager.SubscribeToLanguageChange(language =>
+{
+ Debug.Log($"Language changed to: {language}");
+ // 更新 UI、重新加载资源等
+});
+
+// 取消订阅
+locManager.UnsubscribeFromLanguageChange(callback);
+```
+
+## 高级功能
+
+### 回退机制
+
+当目标语言缺少某个键时,系统会自动回退到默认语言:
+
+```csharp
+// 假设 zhs/common.json 中缺少 "new.feature" 键
+locManager.SetLanguage("zhs");
+var text = locManager.GetText("common", "new.feature");
+// 自动从 eng/common.json 获取
+
+// 回退顺序:
+// 1. 当前语言的覆盖数据
+// 2. 当前语言的原始数据
+// 3. 回退语言的数据
+```
+
+### 覆盖机制
+
+用户可以在 `user://localization_override/` 目录下放置覆盖文件:
+
+```json
+// user://localization_override/eng/common.json
+{
+ "game.title": "My Custom Game Title"
+}
+```
+
+覆盖文件会自动合并到主本地化表中,优先级最高。
+
+### 自定义格式化器
+
+```csharp
+// 实现自定义格式化器
+public class UpperCaseFormatter : ILocalizationFormatter
+{
+ public string Name => "upper";
+
+ public bool TryFormat(string format, object value, IFormatProvider? provider, out string result)
+ {
+ result = value?.ToString()?.ToUpper() ?? string.Empty;
+ return true;
+ }
+}
+
+// 注册格式化器
+locManager.RegisterFormatter("upper", new UpperCaseFormatter());
+
+// 使用格式化器(需要在 LocalizationString 中实现格式化器支持)
+// 格式: {variableName:formatterName:args}
+```
+
+### 内置格式化器
+
+#### ConditionalFormatter
+
+条件格式化器,根据布尔值选择不同文本。
+
+```csharp
+// 格式: {condition:if:trueText|falseText}
+// JSON: "status": "{upgraded:if:Upgraded|Normal}"
+
+var text = locManager.GetString("common", "status")
+ .WithVariable("upgraded", true)
+ .Format();
+// 结果: "Upgraded"
+```
+
+#### PluralFormatter
+
+复数格式化器,根据数量选择单复数形式。
+
+```csharp
+// 格式: {count:plural:singular|plural}
+// JSON: "items": "{count:plural:item|items}"
+
+var text = locManager.GetString("common", "items")
+ .WithVariable("count", 1)
+ .Format();
+// 结果: "item"
+
+var text2 = locManager.GetString("common", "items")
+ .WithVariable("count", 3)
+ .Format();
+// 结果: "items"
+```
+
+## 异常处理
+
+### LocalizationException
+
+本地化异常基类。
+
+### LocalizationKeyNotFoundException
+
+当请求的键不存在时抛出。
+
+```csharp
+try
+{
+ var text = locManager.GetText("common", "nonexistent.key");
+}
+catch (LocalizationKeyNotFoundException ex)
+{
+ Debug.LogError($"Key not found: {ex.TableName}.{ex.Key}");
+}
+```
+
+### LocalizationTableNotFoundException
+
+当请求的表不存在时抛出。
+
+```csharp
+try
+{
+ var table = locManager.GetTable("nonexistent_table");
+}
+catch (LocalizationTableNotFoundException ex)
+{
+ Debug.LogError($"Table not found: {ex.TableName}");
+}
+```
+
+## 最佳实践
+
+### 1. 键名组织
+
+```csharp
+// 推荐:使用层级结构
+"ui.button.start"
+"ui.button.quit"
+"combat.damage.physical"
+"combat.damage.magical"
+
+// 不推荐:扁平结构
+"start_button"
+"quit_button"
+```
+
+### 2. 变量命名
+
+```csharp
+// 推荐:使用驼峰命名
+"{playerName}"
+"{maxHealth}"
+
+// 不推荐:使用下划线或大写
+"{player_name}"
+"{MAX_HEALTH}"
+```
+
+### 3. 表的划分
+
+```csharp
+// 按功能模块划分表
+common.json // 通用文本
+ui.json // UI 文本
+combat.json // 战斗文本
+items.json // 物品文本
+```
+
+### 4. 安全获取
+
+```csharp
+// 推荐:使用 TryGetText 避免异常
+if (locManager.TryGetText("common", "key", out var text))
+{
+ // 使用 text
+}
+
+// 或者提供默认值
+var text = locManager.TryGetText("common", "key", out var result)
+ ? result
+ : "Default Text";
+```
+
+### 5. 语言变化处理
+
+```csharp
+// 在组件初始化时订阅
+public override void OnInit()
+{
+ var locManager = this.GetSystem();
+ locManager.SubscribeToLanguageChange(OnLanguageChanged);
+}
+
+// 在组件销毁时取消订阅
+public override void OnDestroy()
+{
+ var locManager = this.GetSystem();
+ locManager.UnsubscribeFromLanguageChange(OnLanguageChanged);
+}
+
+private void OnLanguageChanged(string language)
+{
+ // 更新 UI
+ UpdateUI();
+}
+```
+
+## 性能考虑
+
+### 缓存策略
+
+- 本地化表在加载后会缓存在内存中
+- 语言切换时只加载新语言的表
+- 建议在游戏启动时预加载常用语言
+
+### 内存优化
+
+```csharp
+// 只加载当前语言,不预加载所有语言
+var config = new LocalizationConfig
+{
+ DefaultLanguage = "eng"
+};
+
+// 按需切换语言
+locManager.SetLanguage(userSelectedLanguage);
+```
+
+## 相关资源
+
+- [Architecture 架构系统](./architecture.md)
+- [System 系统层](./system.md)
+- [Configuration 配置管理](./configuration.md)
From 5fb96761a305acda0eb4d7d888c5a32f96117b3c Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 18 Mar 2026 23:25:09 +0800
Subject: [PATCH 2/8] =?UTF-8?q?docs(localization):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E6=96=87=E6=A1=A3=E5=B9=B6=E5=AE=8C?=
=?UTF-8?q?=E5=96=84=E5=8A=9F=E8=83=BD=E5=AE=9E=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加了配置项暂不支持的说明信息
- 扩展了语言变化监听的使用方式,增加命名方法订阅示例
- 完善了覆盖机制的文档说明
- 优化了异常处理逻辑,精确捕获本地化相关异常
- 实现了正则表达式的预编译以提升性能
- 添加了必要的命名空间引用
---
.../Localization/LocalizationManager.cs | 3 +-
.../Localization/LocalizationString.cs | 19 +++++++++----
docs/zh-CN/core/localization.md | 28 ++++++++++++++-----
3 files changed, 37 insertions(+), 13 deletions(-)
diff --git a/GFramework.Core/Localization/LocalizationManager.cs b/GFramework.Core/Localization/LocalizationManager.cs
index 202bbcd..5ff4d88 100644
--- a/GFramework.Core/Localization/LocalizationManager.cs
+++ b/GFramework.Core/Localization/LocalizationManager.cs
@@ -105,8 +105,9 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
text = GetText(table, key);
return true;
}
- catch
+ catch (LocalizationException)
{
+ // 只捕获本地化相关的异常(键不存在、表不存在等)
text = string.Empty;
return false;
}
diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs
index e15a5c1..0beb7b0 100644
--- a/GFramework.Core/Localization/LocalizationString.cs
+++ b/GFramework.Core/Localization/LocalizationString.cs
@@ -8,6 +8,18 @@ namespace GFramework.Core.Localization;
///
public class LocalizationString : ILocalizationString
{
+ ///
+ /// 匹配 {variableName} 或 {variableName:formatter:args} 的正则表达式模式
+ ///
+ private static readonly string FormatVariablePattern =
+ @"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}";
+
+ ///
+ /// 预编译的静态正则表达式,用于格式化字符串中的变量替换
+ ///
+ private static readonly Regex FormatVariableRegex =
+ new(FormatVariablePattern, RegexOptions.Compiled | RegexOptions.CultureInvariant);
+
private readonly ILocalizationManager _manager;
private readonly Dictionary _variables;
@@ -96,11 +108,8 @@ public class LocalizationString : ILocalizationString
return template;
}
- // 匹配 {variableName} 或 {variableName:formatter:args}
- var pattern = @"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}";
- var regex = new Regex(pattern);
-
- return regex.Replace(template, match =>
+ // 使用预编译的静态正则表达式匹配 {variableName} 或 {variableName:formatter:args}
+ return FormatVariableRegex.Replace(template, match =>
{
var variableName = match.Groups[1].Value;
var formatterName = match.Groups[2].Success ? match.Groups[2].Value : null;
diff --git a/docs/zh-CN/core/localization.md b/docs/zh-CN/core/localization.md
index 440e051..03cb112 100644
--- a/docs/zh-CN/core/localization.md
+++ b/docs/zh-CN/core/localization.md
@@ -103,11 +103,13 @@ bool TryFormat(string format, object value, IFormatProvider? provider, out strin
string DefaultLanguage { get; set; } // 默认语言代码,默认 "eng"
string FallbackLanguage { get; set; } // 回退语言代码,默认 "eng"
string LocalizationPath { get; set; } // 本地化文件路径,默认 "res://localization"
-string OverridePath { get; set; } // 用户覆盖路径,默认 "user://localization_override"
-bool EnableHotReload { get; set; } // 是否启用热重载,默认 true
-bool ValidateOnLoad { get; set; } // 是否在加载时验证,默认 true
+string OverridePath { get; set; } // 用户覆盖路径,默认 "user://localization_override" (暂不支持)
+bool EnableHotReload { get; set; } // 是否启用热重载,默认 true (暂不支持)
+bool ValidateOnLoad { get; set; } // 是否在加载时验证,默认 true (暂不支持)
```
+**注意:** `OverridePath`、`EnableHotReload` 和 `ValidateOnLoad` 配置项已定义但当前版本暂不支持,将在后续版本中实现。
+
## 文件组织
### 目录结构
@@ -247,15 +249,25 @@ foreach (var lang in languages)
### 监听语言变化
```csharp
-// 订阅语言变化事件
+// 方式 1: 使用 lambda 表达式(无法取消订阅)
locManager.SubscribeToLanguageChange(language =>
{
Debug.Log($"Language changed to: {language}");
// 更新 UI、重新加载资源等
});
-// 取消订阅
-locManager.UnsubscribeFromLanguageChange(callback);
+// 方式 2: 使用命名方法(推荐,可以取消订阅)
+void OnLanguageChanged(string language)
+{
+ Debug.Log($"Language changed to: {language}");
+ // 更新 UI、重新加载资源等
+}
+
+// 订阅
+locManager.SubscribeToLanguageChange(OnLanguageChanged);
+
+// 取消订阅(使用相同的方法引用)
+locManager.UnsubscribeFromLanguageChange(OnLanguageChanged);
```
## 高级功能
@@ -278,7 +290,9 @@ var text = locManager.GetText("common", "new.feature");
### 覆盖机制
-用户可以在 `user://localization_override/` 目录下放置覆盖文件:
+**注意:** 覆盖机制功能已规划但当前版本暂不支持,将在后续版本中实现。
+
+未来版本中,用户可以在 `user://localization_override/` 目录下放置覆盖文件:
```json
// user://localization_override/eng/common.json
From 075d397a4c9e151ceb8b728f5ed65cc14cbf0852 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 18 Mar 2026 23:25:55 +0800
Subject: [PATCH 3/8] =?UTF-8?q?refactor(localization):=20=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95=E4=B8=AD=E7=9A=84?=
=?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E5=BC=95=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加对 GFramework.Core.Abstractions.Localization 的引用
- 确保测试文件能够访问本地化相关的抽象接口
- 为后续的本地化功能扩展做好准备
- 保持代码结构的一致性与可维护性
---
.../Localization/LocalizationIntegrationTests.cs | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs b/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
index afce6a2..60d95e0 100644
--- a/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
+++ b/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
@@ -1,3 +1,4 @@
+using GFramework.Core.Abstractions.Localization;
using GFramework.Core.Localization;
namespace GFramework.Core.Tests.Localization;
@@ -5,9 +6,6 @@ namespace GFramework.Core.Tests.Localization;
[TestFixture]
public class LocalizationIntegrationTests
{
- private LocalizationManager? _manager;
- private string _testDataPath = null!;
-
[SetUp]
public void Setup()
{
@@ -25,6 +23,9 @@ public class LocalizationIntegrationTests
_manager.Initialize();
}
+ private LocalizationManager? _manager;
+ private string _testDataPath = null!;
+
[Test]
public void GetText_ShouldReturnEnglishText()
{
From 1f680d28223b45df572056e2307e8a4cf6c6162b Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 19 Mar 2026 09:19:34 +0800
Subject: [PATCH 4/8] =?UTF-8?q?refactor(localization):=20=E9=87=8D?=
=?UTF-8?q?=E6=9E=84=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=AD=97=E7=AC=A6=E4=B8=B2?=
=?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将 FormatVariablePattern 从 readonly 字段改为 const 常量
- 提取复杂的正则替换逻辑到独立的 FormatMatch 方法中
- 新增 GetOptionalGroupValue 辅助方法处理可选组值提取
- 分离格式化值和尝试格式化的逻辑到独立方法
- 简化条件判断并提高代码可读性
- 优化错误处理流程,保持原有功能不变
---
.../Localization/LocalizationString.cs | 83 ++++++++++++-------
1 file changed, 51 insertions(+), 32 deletions(-)
diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs
index 0beb7b0..48eba3a 100644
--- a/GFramework.Core/Localization/LocalizationString.cs
+++ b/GFramework.Core/Localization/LocalizationString.cs
@@ -11,7 +11,7 @@ public class LocalizationString : ILocalizationString
///
/// 匹配 {variableName} 或 {variableName:formatter:args} 的正则表达式模式
///
- private static readonly string FormatVariablePattern =
+ private const string FormatVariablePattern =
@"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}";
///
@@ -109,42 +109,61 @@ public class LocalizationString : ILocalizationString
}
// 使用预编译的静态正则表达式匹配 {variableName} 或 {variableName:formatter:args}
- return FormatVariableRegex.Replace(template, match =>
+ return FormatVariableRegex.Replace(template, match => FormatMatch(match, variables, manager));
+ }
+
+ private static string FormatMatch(
+ Match match,
+ Dictionary variables,
+ ILocalizationManager manager)
+ {
+ var variableName = match.Groups[1].Value;
+ if (!variables.TryGetValue(variableName, out var value))
{
- var variableName = match.Groups[1].Value;
- var formatterName = match.Groups[2].Success ? match.Groups[2].Value : null;
- var formatterArgs = match.Groups[3].Success ? match.Groups[3].Value : null;
+ return match.Value;
+ }
- if (!variables.TryGetValue(variableName, out var value))
- {
- return match.Value; // 保持原样
- }
+ var formatterName = GetOptionalGroupValue(match, 2);
+ if (string.IsNullOrEmpty(formatterName))
+ {
+ return FormatValue(value, manager);
+ }
- // 如果没有格式化器,直接转换为字符串
- if (string.IsNullOrEmpty(formatterName))
- {
- return value switch
- {
- IFormattable formattable => formattable.ToString(null, manager.CurrentCulture),
- _ => value?.ToString() ?? string.Empty
- };
- }
+ return TryFormatValue(match, value, formatterName, manager, out var result)
+ ? result
+ : FormatValue(value, manager);
+ }
- // 尝试使用注册的格式化器
- var formatter = GetFormatter(manager, formatterName);
- if (formatter != null && formatter.TryFormat(formatterArgs ?? string.Empty, value, manager.CurrentCulture,
- out var result))
- {
- return result;
- }
+ private static bool TryFormatValue(
+ Match match,
+ object value,
+ string formatterName,
+ ILocalizationManager manager,
+ out string result)
+ {
+ var formatterArgs = GetOptionalGroupValue(match, 3) ?? string.Empty;
+ if (GetFormatter(manager, formatterName) is { } formatter &&
+ formatter.TryFormat(formatterArgs, value, manager.CurrentCulture, out result))
+ {
+ return true;
+ }
- // 格式化失败,返回基本格式化
- return value switch
- {
- IFormattable formattable => formattable.ToString(null, manager.CurrentCulture),
- _ => value?.ToString() ?? string.Empty
- };
- });
+ result = string.Empty;
+ return false;
+ }
+
+ private static string FormatValue(object value, ILocalizationManager manager)
+ {
+ return value switch
+ {
+ IFormattable formattable => formattable.ToString(null, manager.CurrentCulture),
+ _ => value.ToString() ?? string.Empty
+ };
+ }
+
+ private static string? GetOptionalGroupValue(Match match, int groupIndex)
+ {
+ return match.Groups[groupIndex].Success ? match.Groups[groupIndex].Value : null;
}
///
From b5c67850ce039311fcd6d9130397a9370cfb7cbf Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 19 Mar 2026 09:20:43 +0800
Subject: [PATCH 5/8] =?UTF-8?q?docs(localization):=20=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E5=AD=97=E7=AC=A6=E4=B8=B2=E7=B1=BB?=
=?UTF-8?q?=E7=9A=84XML=E6=96=87=E6=A1=A3=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 为构造函数添加更详细的参数描述和异常说明
- 为WithVariable方法添加完整的XML文档注释
- 为WithVariables方法添加完整的XML文档注释
- 为Format方法添加详细的返回值和备注信息
- 为GetRaw方法添加完整的XML文档注释
- 为Exists方法添加完整的XML文档注释
- 为私有FormatString方法添加参数和返回值说明
- 为私有FormatMatch方法添加详细的处理逻辑描述
- 为私有TryFormatValue方法添加格式化器相关参数说明
- 为私有FormatValue方法添加默认格式化逻辑说明
- 为私有GetOptionalGroupValue方法添加功能说明
- 为私有GetFormatter方法添加获取格式化器的详细描述
---
.../Localization/LocalizationString.cs | 79 ++++++++++++++++---
1 file changed, 70 insertions(+), 9 deletions(-)
diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs
index 48eba3a..15832ab 100644
--- a/GFramework.Core/Localization/LocalizationString.cs
+++ b/GFramework.Core/Localization/LocalizationString.cs
@@ -26,9 +26,10 @@ public class LocalizationString : ILocalizationString
///
/// 初始化本地化字符串
///
- /// 本地化管理器
- /// 表名
- /// 键名
+ /// 本地化管理器实例
+ /// 本地化表名,用于定位本地化资源表
+ /// 本地化键名,用于在表中定位具体的本地化文本
+ /// 当 manager、table 或 key 为 null 时抛出
public LocalizationString(ILocalizationManager manager, string table, string key)
{
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
@@ -43,7 +44,13 @@ public class LocalizationString : ILocalizationString
///
public string Key { get; }
- ///
+ ///
+ /// 添加单个变量到本地化字符串中
+ ///
+ /// 变量名称,用于在模板中匹配对应的占位符
+ /// 变量值,将被转换为字符串并替换到对应位置
+ /// 返回当前的 LocalizationString 实例,支持链式调用
+ /// 当 name 为 null 时抛出
public ILocalizationString WithVariable(string name, object value)
{
if (name == null)
@@ -55,7 +62,12 @@ public class LocalizationString : ILocalizationString
return this;
}
- ///
+ ///
+ /// 批量添加多个变量到本地化字符串中
+ ///
+ /// 变量元组数组,每个元组包含变量名称和对应的值
+ /// 返回当前的 LocalizationString 实例,支持链式调用
+ /// 当 variables 为 null 时抛出
public ILocalizationString WithVariables(params (string name, object value)[] variables)
{
if (variables == null)
@@ -71,14 +83,25 @@ public class LocalizationString : ILocalizationString
return this;
}
- ///
+ ///
+ /// 格式化本地化字符串,将模板中的变量占位符替换为实际值
+ ///
+ /// 格式化后的完整字符串。如果本地化文本不存在,则返回 "[Table.Key]" 格式的占位符
+ ///
+ /// 支持两种格式:
+ /// 1. {variableName} - 简单变量替换
+ /// 2. {variableName:formatter:args} - 使用格式化器进行格式化
+ ///
public string Format()
{
var rawText = GetRaw();
return FormatString(rawText, _variables, _manager);
}
- ///
+ ///
+ /// 获取原始的本地化文本,不进行任何变量替换
+ ///
+ /// 本地化文本。如果在本地化管理器中未找到对应的文本,则返回 "[Table.Key]" 格式的占位符
public string GetRaw()
{
if (!_manager.TryGetText(Table, Key, out var text))
@@ -89,7 +112,10 @@ public class LocalizationString : ILocalizationString
return text;
}
- ///
+ ///
+ /// 检查当前本地化键是否存在于本地化管理器中
+ ///
+ /// 如果存在返回 true;否则返回 false
public bool Exists()
{
return _manager.TryGetText(Table, Key, out _);
@@ -98,6 +124,10 @@ public class LocalizationString : ILocalizationString
///
/// 格式化字符串(支持变量替换和格式化器)
///
+ /// 包含占位符的模板字符串
+ /// 包含变量名称和值的字典
+ /// 本地化管理器实例,用于获取格式化器
+ /// 格式化后的字符串。如果模板为空或 null,则直接返回原模板
private static string FormatString(
string template,
Dictionary variables,
@@ -112,6 +142,13 @@ public class LocalizationString : ILocalizationString
return FormatVariableRegex.Replace(template, match => FormatMatch(match, variables, manager));
}
+ ///
+ /// 处理单个正则表达式匹配项,根据是否有格式化器决定如何处理变量值
+ ///
+ /// 正则表达式匹配结果
+ /// 变量字典
+ /// 本地化管理器实例
+ /// 替换后的字符串。如果变量不存在则返回原始匹配值;如果有格式化器则尝试格式化,失败则使用默认格式化
private static string FormatMatch(
Match match,
Dictionary variables,
@@ -134,6 +171,15 @@ public class LocalizationString : ILocalizationString
: FormatValue(value, manager);
}
+ ///
+ /// 尝试使用指定的格式化器格式化变量值
+ ///
+ /// 正则表达式匹配结果,用于获取格式化参数
+ /// 要格式化的变量值
+ /// 格式化器名称
+ /// 本地化管理器实例
+ /// 格式化后的结果字符串
+ /// 如果格式化成功返回 true;否则返回 false,此时 result 为空字符串
private static bool TryFormatValue(
Match match,
object value,
@@ -152,6 +198,12 @@ public class LocalizationString : ILocalizationString
return false;
}
+ ///
+ /// 对变量值进行默认格式化,不使用自定义格式化器
+ ///
+ /// 要格式化的值
+ /// 本地化管理器实例,提供当前文化信息
+ /// 格式化后的字符串。如果值实现 IFormattable 接口则使用其 ToString 方法,否则调用默认的 ToString 方法
private static string FormatValue(object value, ILocalizationManager manager)
{
return value switch
@@ -161,14 +213,23 @@ public class LocalizationString : ILocalizationString
};
}
+ ///
+ /// 获取正则表达式匹配组中的可选值
+ ///
+ /// 正则表达式匹配结果
+ /// 要获取的组索引
+ /// 如果该组匹配成功则返回其值;否则返回 null
private static string? GetOptionalGroupValue(Match match, int groupIndex)
{
return match.Groups[groupIndex].Success ? match.Groups[groupIndex].Value : null;
}
///
- /// 获取格式化器
+ /// 从本地化管理器获取指定名称的格式化器
///
+ /// 本地化管理器实例
+ /// 格式化器名称
+ /// 如果找到对应的格式化器则返回;否则返回 null
private static ILocalizationFormatter? GetFormatter(ILocalizationManager manager, string name)
{
return manager.GetFormatter(name);
From d7e7d3cc7ffd1ed90d9a724f1320b2d2b2c35a83 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 19 Mar 2026 12:51:59 +0800
Subject: [PATCH 6/8] =?UTF-8?q?feat(events):=20=E4=B8=BA=E4=BC=98=E5=85=88?=
=?UTF-8?q?=E7=BA=A7=E4=BA=8B=E4=BB=B6=E7=B3=BB=E7=BB=9F=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E7=BA=BF=E7=A8=8B=E5=AE=89=E5=85=A8=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加同步锁对象以保护处理器集合的并发访问
- 在注册和注销操作中加入线程安全锁机制
- 实现快照创建方法以避免迭代期间的并发修改
- 重构触发器逻辑以使用线程安全的快照创建
- 在计数器方法中添加同步保护
- 确保所有集合操作都在安全锁内执行
---
GFramework.Core/Events/PriorityEvent.cs | 52 ++++++++++++++++++-------
1 file changed, 39 insertions(+), 13 deletions(-)
diff --git a/GFramework.Core/Events/PriorityEvent.cs b/GFramework.Core/Events/PriorityEvent.cs
index 7abcc1f..8704267 100644
--- a/GFramework.Core/Events/PriorityEvent.cs
+++ b/GFramework.Core/Events/PriorityEvent.cs
@@ -18,6 +18,11 @@ public class PriorityEvent : IEvent
///
private readonly List _handlers = new();
+ ///
+ /// 保护处理器集合的并发访问
+ ///
+ private readonly object _syncRoot = new();
+
///
/// 标记事件是否已被处理(用于 UntilHandled 传播模式)
///
@@ -52,10 +57,13 @@ public class PriorityEvent : IEvent
public IUnRegister Register(Action onEvent, int priority)
{
var handler = new EventHandler(onEvent, priority);
- _handlers.Add(handler);
+ lock (_syncRoot)
+ {
+ _handlers.Add(handler);
- // 按优先级降序排序(高优先级在前)
- _handlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
+ // 按优先级降序排序(高优先级在前)
+ _handlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
+ }
return new DefaultUnRegister(() => UnRegister(onEvent));
}
@@ -66,7 +74,10 @@ public class PriorityEvent : IEvent
/// 需要被注销的事件处理方法
public void UnRegister(Action onEvent)
{
- _handlers.RemoveAll(h => h.Handler == onEvent);
+ lock (_syncRoot)
+ {
+ _handlers.RemoveAll(h => h.Handler == onEvent);
+ }
}
///
@@ -78,10 +89,13 @@ public class PriorityEvent : IEvent
public IUnRegister RegisterWithContext(Action> onEvent, int priority = 0)
{
var handler = new ContextEventHandler(onEvent, priority);
- _contextHandlers.Add(handler);
+ lock (_syncRoot)
+ {
+ _contextHandlers.Add(handler);
- // 按优先级降序排序(高优先级在前)
- _contextHandlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
+ // 按优先级降序排序(高优先级在前)
+ _contextHandlers.Sort((a, b) => b.Priority.CompareTo(a.Priority));
+ }
return new DefaultUnRegister(() => UnRegisterContext(onEvent));
}
@@ -92,7 +106,10 @@ public class PriorityEvent : IEvent
/// 需要被注销的事件处理方法
public void UnRegisterContext(Action> onEvent)
{
- _contextHandlers.RemoveAll(h => h.Handler == onEvent);
+ lock (_syncRoot)
+ {
+ _contextHandlers.RemoveAll(h => h.Handler == onEvent);
+ }
}
///
@@ -172,8 +189,7 @@ public class PriorityEvent : IEvent
/// 事件参数
private void TriggerHighest(T t)
{
- var normalSnapshot = _handlers.ToArray();
- var contextSnapshot = _contextHandlers.ToArray();
+ var (normalSnapshot, contextSnapshot) = CreateSnapshots();
var highestPriority = GetHighestPriority(normalSnapshot, contextSnapshot);
if (highestPriority != int.MinValue)
@@ -191,8 +207,7 @@ public class PriorityEvent : IEvent
private List<(int Priority, Action? Handler, Action>? ContextHandler, bool IsContext)>
MergeAndSortHandlers(T t)
{
- var normalSnapshot = _handlers.ToArray();
- var contextSnapshot = _contextHandlers.ToArray();
+ var (normalSnapshot, contextSnapshot) = CreateSnapshots();
// 使用快照避免迭代期间修改
return normalSnapshot
.Select(h => (h.Priority, Handler: (Action?)(() => h.Handler.Invoke(t)),
@@ -260,7 +275,18 @@ public class PriorityEvent : IEvent
/// 监听器总数量
public int GetListenerCount()
{
- return _handlers.Count + _contextHandlers.Count;
+ lock (_syncRoot)
+ {
+ return _handlers.Count + _contextHandlers.Count;
+ }
+ }
+
+ private (EventHandler[] NormalHandlers, ContextEventHandler[] ContextHandlers) CreateSnapshots()
+ {
+ lock (_syncRoot)
+ {
+ return (_handlers.ToArray(), _contextHandlers.ToArray());
+ }
}
///
From 0c5c9dceae63e57f118907c73a47c967c38e5f5d Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 19 Mar 2026 12:52:11 +0800
Subject: [PATCH 7/8] =?UTF-8?q?test(localization):=20=E6=B7=BB=E5=8A=A0?=
=?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E9=9B=86=E6=88=90=E6=B5=8B=E8=AF=95?=
=?UTF-8?q?=E7=9A=84=E4=B8=B4=E6=97=B6=E6=96=87=E4=BB=B6=E7=AE=A1=E7=90=86?=
=?UTF-8?q?=E5=92=8C=E6=B5=8B=E8=AF=95=E6=95=B0=E6=8D=AE=E5=88=9B=E5=BB=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加 System.IO 命名空间引用以支持文件操作
- 实现 CreateTestLocalizationFiles 方法创建测试用的多语言文件
- 使用 GUID 生成唯一的临时目录路径避免冲突
- 添加 TearDown 方法清理测试过程中创建的临时文件
- 在 Setup 方法中调用文件创建方法初始化测试环境
- 将目标框架配置改为可配置的条件变量方式
---
.../GFramework.Core.Tests.csproj | 3 +-
.../LocalizationIntegrationTests.cs | 38 ++++++++++++++++++-
.../GFramework.Ecs.Arch.Tests.csproj | 3 +-
.../GFramework.Game.Tests.csproj | 3 +-
.../GFramework.SourceGenerators.Tests.csproj | 3 +-
5 files changed, 45 insertions(+), 5 deletions(-)
diff --git a/GFramework.Core.Tests/GFramework.Core.Tests.csproj b/GFramework.Core.Tests/GFramework.Core.Tests.csproj
index b71bb9f..b8f7997 100644
--- a/GFramework.Core.Tests/GFramework.Core.Tests.csproj
+++ b/GFramework.Core.Tests/GFramework.Core.Tests.csproj
@@ -1,7 +1,8 @@
- net8.0;net10.0
+ net10.0
+ $(TestTargetFrameworks)
disable
enable
false
diff --git a/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs b/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
index 60d95e0..bfcf15e 100644
--- a/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
+++ b/GFramework.Core.Tests/Localization/LocalizationIntegrationTests.cs
@@ -1,3 +1,4 @@
+using System.IO;
using GFramework.Core.Abstractions.Localization;
using GFramework.Core.Localization;
@@ -9,7 +10,9 @@ public class LocalizationIntegrationTests
[SetUp]
public void Setup()
{
- _testDataPath = "/tmp/localization_example";
+ _testDataPath = Path.Combine(Path.GetTempPath(), $"gframework_localization_{Guid.NewGuid():N}");
+ CreateTestLocalizationFiles(_testDataPath);
+
var config = new LocalizationConfig
{
DefaultLanguage = "eng",
@@ -23,9 +26,42 @@ public class LocalizationIntegrationTests
_manager.Initialize();
}
+ [TearDown]
+ public void TearDown()
+ {
+ if (Directory.Exists(_testDataPath))
+ {
+ Directory.Delete(_testDataPath, recursive: true);
+ }
+ }
+
private LocalizationManager? _manager;
private string _testDataPath = null!;
+ private static void CreateTestLocalizationFiles(string rootPath)
+ {
+ var engPath = Path.Combine(rootPath, "eng");
+ var zhsPath = Path.Combine(rootPath, "zhs");
+ Directory.CreateDirectory(engPath);
+ Directory.CreateDirectory(zhsPath);
+
+ File.WriteAllText(Path.Combine(engPath, "common.json"), """
+ {
+ "game.title": "My Game",
+ "ui.message.welcome": "Welcome, {playerName}!",
+ "status.health": "Health: {current}/{max}"
+ }
+ """);
+
+ File.WriteAllText(Path.Combine(zhsPath, "common.json"), """
+ {
+ "game.title": "我的游戏",
+ "ui.message.welcome": "欢迎, {playerName}!",
+ "status.health": "生命值: {current}/{max}"
+ }
+ """);
+ }
+
[Test]
public void GetText_ShouldReturnEnglishText()
{
diff --git a/GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj b/GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj
index 5fac8dd..b4eb273 100644
--- a/GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj
+++ b/GFramework.Ecs.Arch.Tests/GFramework.Ecs.Arch.Tests.csproj
@@ -1,7 +1,8 @@
- net8.0;net10.0
+ net10.0
+ $(TestTargetFrameworks)
disable
enable
false
diff --git a/GFramework.Game.Tests/GFramework.Game.Tests.csproj b/GFramework.Game.Tests/GFramework.Game.Tests.csproj
index 56fa021..f210018 100644
--- a/GFramework.Game.Tests/GFramework.Game.Tests.csproj
+++ b/GFramework.Game.Tests/GFramework.Game.Tests.csproj
@@ -1,7 +1,8 @@
- net8.0;net10.0
+ net10.0
+ $(TestTargetFrameworks)
disable
enable
false
diff --git a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj
index 733f870..48c8406 100644
--- a/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj
+++ b/GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj
@@ -1,7 +1,8 @@
- net8.0;net10.0
+ net10.0
+ $(TestTargetFrameworks)
disable
enable
false
From 9ca28a44d8544db980df4354f07641422fb79e11 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Thu, 19 Mar 2026 13:15:39 +0800
Subject: [PATCH 8/8] =?UTF-8?q?refactor(localization):=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E5=9B=9E=E9=80=80=E8=A1=A8=E6=A3=80=E6=9F=A5=E9=80=BB?=
=?UTF-8?q?=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将 Fallback.ContainsKey 检查替换为更安全的模式匹配语法
- 避免了潜在的空引用异常风险
- 提高了代码的可读性和健壮性
- 保持了原有的功能逻辑不变
---
GFramework.Core/Localization/LocalizationTable.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/GFramework.Core/Localization/LocalizationTable.cs b/GFramework.Core/Localization/LocalizationTable.cs
index 2a063f2..5109089 100644
--- a/GFramework.Core/Localization/LocalizationTable.cs
+++ b/GFramework.Core/Localization/LocalizationTable.cs
@@ -79,9 +79,9 @@ public class LocalizationTable : ILocalizationTable
}
// 最后尝试回退表
- if (Fallback != null && Fallback.ContainsKey(key))
+ if (Fallback is { } fb && fb.ContainsKey(key))
{
- return Fallback.GetRawText(key);
+ return fb.GetRawText(key);
}
throw new LocalizationKeyNotFoundException(Name, key);
@@ -96,7 +96,7 @@ public class LocalizationTable : ILocalizationTable
{
return _overrides.ContainsKey(key)
|| _data.ContainsKey(key)
- || (Fallback?.ContainsKey(key) ?? false);
+ || (Fallback is { } fb && fb.ContainsKey(key));
}
///