312 lines
14 KiB
C#
312 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Resources;
|
|
using System.Threading;
|
|
using Prism.Events;
|
|
using XP.Common.Localization.Enums;
|
|
using XP.Common.Localization.Events;
|
|
using XP.Common.Localization.Interfaces;
|
|
using XP.Common.Logging.Interfaces;
|
|
|
|
namespace XP.Common.Localization.Implementations
|
|
{
|
|
/// <summary>
|
|
/// Resx 本地化服务实现 | Resx localization service implementation
|
|
/// 使用 .NET ResourceManager 加载 Resx 资源文件 | Uses .NET ResourceManager to load Resx resource files
|
|
/// 支持多资源源 Fallback Chain 机制 | Supports multi-source Fallback Chain mechanism
|
|
/// </summary>
|
|
public class ResxLocalizationService : ILocalizationService
|
|
{
|
|
private readonly List<ResourceSource> _resourceSources = new List<ResourceSource>();
|
|
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
|
|
private readonly IEventAggregator _eventAggregator;
|
|
private readonly ILocalizationConfig _config;
|
|
private readonly ILoggerService _logger;
|
|
private SupportedLanguage _currentLanguage;
|
|
|
|
/// <summary>
|
|
/// 获取当前语言 | Get current language
|
|
/// </summary>
|
|
public SupportedLanguage CurrentLanguage => _currentLanguage;
|
|
|
|
/// <summary>
|
|
/// 语言切换事件 | Language changed event
|
|
/// </summary>
|
|
public event EventHandler<LanguageChangedEventArgs>? LanguageChanged;
|
|
|
|
/// <summary>
|
|
/// 构造函数 | Constructor
|
|
/// </summary>
|
|
/// <param name="eventAggregator">事件聚合器 | Event aggregator</param>
|
|
/// <param name="config">本地化配置 | Localization configuration</param>
|
|
/// <param name="logger">日志服务 | Logger service</param>
|
|
public ResxLocalizationService(
|
|
IEventAggregator eventAggregator,
|
|
ILocalizationConfig config,
|
|
ILoggerService logger)
|
|
{
|
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
|
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<ResxLocalizationService>();
|
|
|
|
// 初始化默认资源源(XP.Common),注册到 Fallback Chain[0]
|
|
// Initialize default resource source (XP.Common), registered at Fallback Chain[0]
|
|
var defaultResourceManager = new ResourceManager(
|
|
"XP.Common.Resources.Resources",
|
|
Assembly.GetExecutingAssembly());
|
|
_resourceSources.Add(new ResourceSource("XP.Common", defaultResourceManager));
|
|
|
|
// 加载保存的语言或使用默认语言 | Load saved language or use default language
|
|
InitializeLanguage();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
|
|
/// </summary>
|
|
/// <param name="key">资源键 | Resource key</param>
|
|
/// <returns>本地化字符串 | Localized string</returns>
|
|
public string GetString(string key)
|
|
{
|
|
return GetString(key, CultureInfo.CurrentUICulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取本地化字符串(指定语言)| Get localized string (specified language)
|
|
/// 从 Fallback Chain 末尾向前遍历,第一个返回非 null 值的 ResourceManager 即为结果
|
|
/// Traverses the Fallback Chain from end to beginning, first ResourceManager returning non-null wins
|
|
/// </summary>
|
|
/// <param name="key">资源键 | Resource key</param>
|
|
/// <param name="culture">文化信息 | Culture info</param>
|
|
/// <returns>本地化字符串 | Localized string</returns>
|
|
public string GetString(string key, CultureInfo culture)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
_rwLock.EnterReadLock();
|
|
try
|
|
{
|
|
// 从末尾向前遍历(后注册 = 高优先级)
|
|
// Traverse from end to beginning (last registered = highest priority)
|
|
for (int i = _resourceSources.Count - 1; i >= 0; i--)
|
|
{
|
|
try
|
|
{
|
|
var value = _resourceSources[i].ResourceManager.GetString(key, culture);
|
|
if (value != null)
|
|
{
|
|
return value;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// 单个 ResourceManager 抛出异常时捕获并继续遍历下一个
|
|
// Catch exception from individual ResourceManager and continue to next
|
|
_logger.Error(ex, $"资源源 '{_resourceSources[i].Name}' 查找键 '{key}' 时出错 | Error looking up key '{key}' in resource source '{_resourceSources[i].Name}'");
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_rwLock.ExitReadLock();
|
|
}
|
|
|
|
// 全部未找到则返回 key 本身并记录警告日志
|
|
// If all sources fail to find the key, return the key itself and log warning
|
|
_logger.Warn($"资源键未找到 | Resource key not found: '{key}' for culture '{culture?.Name ?? "null"}'");
|
|
return key;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
|
|
/// Register a module resource source to the end of the Fallback Chain (highest priority)
|
|
/// </summary>
|
|
/// <param name="name">资源源名称 | Resource source name</param>
|
|
/// <param name="resourceManager">模块的 ResourceManager 实例 | Module's ResourceManager instance</param>
|
|
public void RegisterResourceSource(string name, ResourceManager resourceManager)
|
|
{
|
|
if (name == null) throw new ArgumentNullException(nameof(name));
|
|
if (resourceManager == null) throw new ArgumentNullException(nameof(resourceManager));
|
|
|
|
_rwLock.EnterWriteLock();
|
|
try
|
|
{
|
|
// 检查重复名称 | Check for duplicate name
|
|
if (_resourceSources.Any(s => s.Name == name))
|
|
{
|
|
_logger.Warn($"资源源 '{name}' 已存在,注册被拒绝 | Resource source '{name}' already exists, registration rejected");
|
|
throw new InvalidOperationException($"Resource source '{name}' is already registered.");
|
|
}
|
|
|
|
_resourceSources.Add(new ResourceSource(name, resourceManager));
|
|
_logger.Info($"资源源 '{name}' 已注册到 Fallback Chain | Resource source '{name}' registered to Fallback Chain");
|
|
}
|
|
finally
|
|
{
|
|
_rwLock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从 Fallback Chain 中注销指定资源源
|
|
/// Unregister the specified resource source from the Fallback Chain
|
|
/// </summary>
|
|
/// <param name="name">资源源名称 | Resource source name</param>
|
|
public void UnregisterResourceSource(string name)
|
|
{
|
|
if (name == null) throw new ArgumentNullException(nameof(name));
|
|
|
|
// 禁止注销默认资源源 | Prevent unregistering default resource source
|
|
if (name == "XP.Common")
|
|
{
|
|
throw new InvalidOperationException("Cannot unregister the default resource source 'XP.Common'.");
|
|
}
|
|
|
|
_rwLock.EnterWriteLock();
|
|
try
|
|
{
|
|
var source = _resourceSources.FirstOrDefault(s => s.Name == name);
|
|
if (source != null)
|
|
{
|
|
_resourceSources.Remove(source);
|
|
_logger.Info($"资源源 '{name}' 已从 Fallback Chain 注销 | Resource source '{name}' unregistered from Fallback Chain");
|
|
}
|
|
else
|
|
{
|
|
// 注销不存在的名称时静默忽略,记录警告日志
|
|
// Silently ignore non-existent name, log warning
|
|
_logger.Warn($"资源源 '{name}' 不存在,注销操作被忽略 | Resource source '{name}' does not exist, unregister ignored");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_rwLock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置当前语言(仅保存配置,重启后生效)| Set current language (save config only, takes effect after restart)
|
|
/// </summary>
|
|
/// <param name="language">目标语言 | Target language</param>
|
|
public void SetLanguage(SupportedLanguage language)
|
|
{
|
|
_rwLock.EnterWriteLock();
|
|
try
|
|
{
|
|
// 如果语言相同,无需保存 | If language is the same, no need to save
|
|
if (_currentLanguage == language)
|
|
{
|
|
_logger.Info($"语言已经是 {language},无需切换 | Language is already {language}, no need to switch");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_logger.Info($"保存语言设置,重启后生效 | Saving language setting, takes effect after restart: {_currentLanguage} -> {language}");
|
|
|
|
// 仅保存到配置,不更新运行时状态 | Only save to config, do not update runtime state
|
|
_config.SaveLanguage(language);
|
|
|
|
_logger.Info($"语言设置已保存 | Language setting saved: {language}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, $"语言设置保存失败 | Failed to save language setting: {language}");
|
|
throw;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_rwLock.ExitWriteLock();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取所有支持的语言 | Get all supported languages
|
|
/// </summary>
|
|
/// <returns>支持的语言列表 | List of supported languages</returns>
|
|
public IEnumerable<SupportedLanguage> GetSupportedLanguages()
|
|
{
|
|
return Enum.GetValues(typeof(SupportedLanguage))
|
|
.Cast<SupportedLanguage>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 初始化语言设置 | Initialize language settings
|
|
/// </summary>
|
|
private void InitializeLanguage()
|
|
{
|
|
try
|
|
{
|
|
// 尝试获取保存的语言 | Try to get saved language
|
|
var savedLanguage = _config.GetSavedLanguage();
|
|
var language = savedLanguage ?? _config.GetSystemDefaultLanguage();
|
|
|
|
_currentLanguage = language;
|
|
var culture = GetCultureInfo(language);
|
|
CultureInfo.CurrentUICulture = culture;
|
|
CultureInfo.CurrentCulture = culture;
|
|
|
|
_logger.Info($"语言初始化完成 | Language initialized: {language} (Culture: {culture.Name})");
|
|
|
|
// 验证资源文件是否可加载 | Verify resource file can be loaded
|
|
ValidateResourceFile(culture);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "语言初始化失败 | Language initialization failed");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取语言对应的 CultureInfo | Get CultureInfo for language
|
|
/// </summary>
|
|
/// <param name="language">支持的语言 | Supported language</param>
|
|
/// <returns>文化信息 | Culture info</returns>
|
|
private CultureInfo GetCultureInfo(SupportedLanguage language)
|
|
{
|
|
return language switch
|
|
{
|
|
SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
|
|
SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
|
|
SupportedLanguage.EnUS => new CultureInfo("en-US"),
|
|
_ => new CultureInfo("zh-CN") // 默认简体中文 | Default to Simplified Chinese
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 验证资源文件是否可加载 | Validate resource file can be loaded
|
|
/// 使用默认资源源(XP.Common)进行验证 | Uses default resource source (XP.Common) for validation
|
|
/// </summary>
|
|
/// <param name="culture">文化信息 | Culture info</param>
|
|
private void ValidateResourceFile(CultureInfo culture)
|
|
{
|
|
try
|
|
{
|
|
// 使用第一个资源源(XP.Common)进行验证
|
|
// Use the first resource source (XP.Common) for validation
|
|
var testValue = _resourceSources[0].ResourceManager.GetString("App_Title", culture);
|
|
|
|
if (testValue != null)
|
|
{
|
|
_logger.Info($"资源文件验证成功 | Resource file validated for culture: {culture.Name}");
|
|
}
|
|
else
|
|
{
|
|
_logger.Warn($"资源文件可能不完整 | Resource file may be incomplete for culture: {culture.Name}");
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
_logger.Warn($"资源文件验证失败,将使用默认资源 | Resource file validation failed, will use default resources for culture: {culture.Name}");
|
|
}
|
|
}
|
|
}
|
|
}
|