# 多语言支持使用指南 | Localization Support Guide
## 概述 | Overview
XplorePlane 多语言支持系统基于 .NET 原生 Resx 资源文件实现,与 Prism MVVM 架构无缝集成。系统支持简体中文(zh-CN)、繁体中文(zh-TW)和美式英语(en-US)三种语言。
### 核心特性 | Key Features
- ✅ 基于 .NET Resx 资源文件,编译时类型安全
- ✅ 简洁的 XAML 标记扩展语法
- ✅ 完整的 ViewModel 集成支持
- ✅ 语言设置持久化到 App.config
- ✅ 跨模块事件通知机制
- ✅ 健壮的错误处理和回退机制
- ✅ 模块化资源管理:各子项目独立维护 resx,通过 Fallback Chain 自动查找(v2.0)
- ✅ 线程安全的资源源注册/注销(v2.0)
---
## 快速开始 | Quick Start
### 1. 在 XAML 中使用本地化资源 | Using Localization in XAML
#### 基础用法 | Basic Usage
```xml
```
#### 命名空间声明 | Namespace Declaration
在 XAML 文件顶部添加命名空间引用:
```xml
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
```
#### 语法说明 | Syntax Explanation
- `{loc:Localization Key=ResourceKey}` - 完整语法
- `{loc:Localization App_Title}` - 简化语法(Key 可省略)
- 资源键不存在时,显示键名本身(便于调试)
---
### 2. 在 C# 代码中使用静态帮助类 | Using Static Helper in C# Code
#### LocalizationHelper 静态帮助类 | LocalizationHelper Static Helper
适用于不方便依赖注入的场景,如静态方法、工具类、简单的代码调用等。
文件位置:`XP.Common/Localization/Helpers/LocalizationHelper.cs`
```csharp
using XP.Common.Localization;
// 基本用法 | Basic usage
var title = LocalizationHelper.Get("App_Title");
// 带格式化参数 | With format arguments
var errorMsg = LocalizationHelper.Get("Settings_Language_SwitchFailed", ex.Message);
```
#### 初始化(V1.1 新增)| Initialization (New in V1.1)
V1.1 版本新增 `Initialize()` 方法,建议在 `CommonModule` 或 App 启动时调用,使 `LocalizationHelper` 通过 `ILocalizationService` 获取字符串,从而支持 Fallback Chain 多资源源查找。
```csharp
// 在 CommonModule 或 App 启动时调用 | Call at CommonModule or App startup
LocalizationHelper.Initialize(localizationService);
```
初始化后,`Get()` 优先通过 `ILocalizationService` 获取(支持 Fallback Chain);未初始化时兼容回退到原始 `ResourceManager`。
#### 与 ILocalizationService 的区别 | Difference from ILocalizationService
| 特性 | LocalizationHelper | ILocalizationService |
|------|-------------------|---------------------|
| 调用方式 | 静态方法,直接调用 | 依赖注入 |
| 适用场景 | 工具类、静态方法、简单调用 | ViewModel、Service 等 DI 管理的类 |
| 语言来源 | `CultureInfo.CurrentUICulture` | `CultureInfo.CurrentUICulture` |
| 资源来源 | 初始化后走 Fallback Chain;未初始化时仅 XP.Common 资源 | Fallback Chain(所有已注册资源源) |
| 找不到键时 | 返回键本身 | 返回键本身 + 记录警告日志 |
| Fallback Chain 支持 | ✅ 需先调用 `Initialize()` | ✅ 原生支持 |
#### 使用建议 | Usage Recommendations
- 在 ViewModel / Service 中优先使用 `ILocalizationService`(可测试、可 Mock)
- 在静态方法、工具类、或不方便注入的地方使用 `LocalizationHelper`
- 建议在应用启动时调用 `LocalizationHelper.Initialize()` 以启用 Fallback Chain 支持
- 两者初始化后读取同一套资源,结果一致
---
### 3. 在 ViewModel 中使用本地化服务 | Using Localization Service in ViewModel
#### 依赖注入 | Dependency Injection
```csharp
using XplorePlane.Common.Localization.Interfaces;
using XplorePlane.Common.Localization.Enums;
using Prism.Mvvm;
namespace XplorePlane.App.ViewModels
{
public class MyViewModel : BindableBase
{
private readonly ILocalizationService _localizationService;
private readonly ILoggerService _logger;
public MyViewModel(
ILocalizationService localizationService,
ILoggerService logger)
{
_localizationService = localizationService;
_logger = logger;
}
// 获取本地化字符串 | Get localized string
public string GetWelcomeMessage()
{
return _localizationService.GetString("Welcome_Message");
}
// 获取当前语言 | Get current language
public SupportedLanguage CurrentLanguage => _localizationService.CurrentLanguage;
}
}
```
#### 动态文本绑定 | Dynamic Text Binding
```csharp
public class StatusViewModel : BindableBase
{
private readonly ILocalizationService _localizationService;
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
set => SetProperty(ref _statusMessage, value);
}
public StatusViewModel(ILocalizationService localizationService)
{
_localizationService = localizationService;
// 订阅语言切换事件 | Subscribe to language changed event
_localizationService.LanguageChanged += OnLanguageChanged;
// 初始化状态消息 | Initialize status message
UpdateStatusMessage();
}
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
// 语言切换时更新文本 | Update text when language changes
UpdateStatusMessage();
}
private void UpdateStatusMessage()
{
StatusMessage = _localizationService.GetString("Status_Ready");
}
}
```
#### 数据验证消息本地化 | Localized Validation Messages
```csharp
public class FormViewModel : BindableBase, IDataErrorInfo
{
private readonly ILocalizationService _localizationService;
private string _username;
public string Username
{
get => _username;
set => SetProperty(ref _username, value);
}
public string this[string columnName]
{
get
{
if (columnName == nameof(Username))
{
if (string.IsNullOrWhiteSpace(Username))
{
return _localizationService.GetString("Validation_UsernameRequired");
}
if (Username.Length < 3)
{
return _localizationService.GetString("Validation_UsernameTooShort");
}
}
return null;
}
}
public string Error => null;
}
```
---
### 4. 实现语言切换器 | Implementing Language Switcher
#### ViewModel 实现 | ViewModel Implementation
```csharp
using XplorePlane.Common.Localization.Interfaces;
using XplorePlane.Common.Localization.Enums;
using XplorePlane.Common.Localization.ViewModels;
using Prism.Mvvm;
using System.Collections.Generic;
using System.Windows;
namespace XplorePlane.App.ViewModels
{
public class SettingsViewModel : BindableBase
{
private readonly ILocalizationService _localizationService;
private readonly ILoggerService _logger;
private SupportedLanguage _selectedLanguage;
public SupportedLanguage SelectedLanguage
{
get => _selectedLanguage;
set
{
if (SetProperty(ref _selectedLanguage, value))
{
ChangeLanguage(value);
}
}
}
public IEnumerable AvailableLanguages { get; }
public SettingsViewModel(
ILocalizationService localizationService,
ILoggerService logger)
{
_localizationService = localizationService;
_logger = logger;
_selectedLanguage = localizationService.CurrentLanguage;
// 初始化可用语言列表 | Initialize available languages
AvailableLanguages = new[]
{
new LanguageOption(SupportedLanguage.ZhCN, "简体中文", "🇨🇳"),
new LanguageOption(SupportedLanguage.ZhTW, "繁體中文", "🇹🇼"),
new LanguageOption(SupportedLanguage.EnUS, "English", "🇺🇸")
};
}
private void ChangeLanguage(SupportedLanguage language)
{
try
{
_localizationService.SetLanguage(language);
// 提示用户重启应用 | Prompt user to restart
var message = _localizationService.GetString("Message_RestartRequired");
var title = _localizationService.GetString("Title_Notice");
MessageBox.Show(
message,
title,
MessageBoxButton.OK,
MessageBoxImage.Information);
_logger.Information($"Language changed to {language}");
}
catch (Exception ex)
{
_logger.Error($"Failed to change language to {language}", ex);
var errorMessage = _localizationService.GetString("Error_LanguageChangeFailed");
MessageBox.Show(
errorMessage,
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
}
}
```
#### XAML 视图 | XAML View
```xml
```
---
### 5. 订阅语言切换事件 | Subscribing to Language Changed Events
#### 使用 Prism EventAggregator | Using Prism EventAggregator
```csharp
using Prism.Events;
using XplorePlane.Common.Localization.Events;
public class MyViewModel : BindableBase
{
private readonly IEventAggregator _eventAggregator;
private readonly ILocalizationService _localizationService;
public MyViewModel(
IEventAggregator eventAggregator,
ILocalizationService localizationService)
{
_eventAggregator = eventAggregator;
_localizationService = localizationService;
// 订阅语言切换事件 | Subscribe to language changed event
_eventAggregator.GetEvent()
.Subscribe(OnLanguageChanged, ThreadOption.UIThread);
}
private void OnLanguageChanged(LanguageChangedEventArgs args)
{
// 处理语言切换 | Handle language change
Console.WriteLine($"Language changed from {args.OldLanguage} to {args.NewLanguage}");
// 刷新 UI 文本 | Refresh UI text
RefreshLocalizedContent();
}
private void RefreshLocalizedContent()
{
// 更新所有需要本地化的属性 | Update all localized properties
RaisePropertyChanged(nameof(Title));
RaisePropertyChanged(nameof(Description));
}
}
```
#### 使用 ILocalizationService 事件 | Using ILocalizationService Event
```csharp
public class MyViewModel : BindableBase
{
private readonly ILocalizationService _localizationService;
public MyViewModel(ILocalizationService localizationService)
{
_localizationService = localizationService;
// 订阅服务事件 | Subscribe to service event
_localizationService.LanguageChanged += OnLanguageChanged;
}
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
// 处理语言切换 | Handle language change
RefreshLocalizedContent();
}
}
```
---
### 6. 多资源源 Fallback Chain | Multi-Source Fallback Chain
V1.1 版本引入了多资源源 Fallback Chain 机制,允许各 Prism 模块注册自己的 Resx 资源文件到统一的查找链中。
#### 架构说明 | Architecture
```
Fallback Chain(查找顺序从右到左 | Lookup order from right to left):
[XP.Common (默认)] → [XP.Scan (模块注册)] → [XP.Hardware (模块注册)]
↑ 最高优先级 | Highest priority
```
- `XP.Common` 为默认资源源,始终位于 Chain[0],不可注销
- 后注册的模块优先级更高(从末尾向前遍历)
- 单个资源源查找异常时自动跳过,继续遍历下一个
- 全部未找到时返回 key 本身并记录警告日志
- 线程安全:内部使用 `ReaderWriterLockSlim` 保护读写操作
#### 注册模块资源源 | Register Module Resource Source
在 Prism 模块的 `OnInitialized` 中注册:
```csharp
using System.Resources;
using XP.Common.Localization.Interfaces;
public class ScanModule : IModule
{
private readonly ILocalizationService _localizationService;
public ScanModule(ILocalizationService localizationService)
{
_localizationService = localizationService;
}
public void OnInitialized(IContainerProvider containerProvider)
{
// 注册模块资源源到 Fallback Chain | Register module resource source to Fallback Chain
var resourceManager = new ResourceManager(
"XP.Scan.Resources.Resources",
typeof(ScanModule).Assembly);
_localizationService.RegisterResourceSource("XP.Scan", resourceManager);
}
public void RegisterTypes(IContainerRegistry containerRegistry) { }
}
```
#### 注销模块资源源 | Unregister Module Resource Source
```csharp
// 注销指定资源源 | Unregister specified resource source
_localizationService.UnregisterResourceSource("XP.Scan");
```
#### 注意事项 | Notes
| 场景 | 行为 |
|------|------|
| 重复注册同名资源源 | 抛出 `InvalidOperationException` |
| 注销 `"XP.Common"` | 抛出 `InvalidOperationException`(禁止注销默认资源源) |
| 注销不存在的名称 | 静默忽略,记录警告日志 |
| 单个 ResourceManager 抛异常 | 捕获异常并继续遍历下一个资源源 |
#### 模块资源文件结构 | Module Resource File Structure
各模块应在自己的项目中创建独立的资源文件:
```
XP.Scan/
├── Resources/
│ ├── Resources.resx # 默认(简体中文)
│ ├── Resources.zh-CN.resx # 简体中文
│ ├── Resources.zh-TW.resx # 繁体中文
│ └── Resources.en-US.resx # 英文
```
资源键建议使用模块前缀命名(如 `Scan_Button_Start`),避免与其他模块冲突。当多个资源源存在同名键时,后注册的模块优先。
---
## 添加新语言 | Adding New Languages
### 步骤 1: 更新枚举定义 | Step 1: Update Enum Definition
编辑 `XplorePlane.Common/Localization/Enums/SupportedLanguage.cs`:
```csharp
public enum SupportedLanguage
{
[Description("zh-CN")]
ZhCN,
[Description("zh-TW")]
ZhTW,
[Description("en-US")]
EnUS,
// 添加新语言 | Add new language
[Description("ja-JP")]
JaJP, // 日语 | Japanese
[Description("ko-KR")]
KoKR // 韩语 | Korean
}
```
### 步骤 2: 创建资源文件 | Step 2: Create Resource File
1. 在 `XplorePlane.Common/Resources/` 目录下创建新的资源文件
2. 文件命名格式:`Resources..resx`
3. 例如:`Resources.ja-JP.resx`、`Resources.ko-KR.resx`
### 步骤 3: 添加翻译内容 | Step 3: Add Translations
在新资源文件中添加所有资源键的翻译:
```xml
XplorePlane X線検査システム
ファイル
```
### 步骤 4: 更新 GetCultureInfo 方法 | Step 4: Update GetCultureInfo Method
编辑 `ResxLocalizationService.cs` 中的 `GetCultureInfo` 方法:
```csharp
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"),
SupportedLanguage.JaJP => new CultureInfo("ja-JP"), // 新增 | New
SupportedLanguage.KoKR => new CultureInfo("ko-KR"), // 新增 | New
_ => new CultureInfo("zh-CN")
};
}
```
### 步骤 5: 更新系统默认语言逻辑 | Step 5: Update System Default Language Logic
编辑 `LocalizationConfig.cs` 中的 `GetSystemDefaultLanguage` 方法:
```csharp
public SupportedLanguage GetSystemDefaultLanguage()
{
try
{
var systemCulture = CultureInfo.CurrentUICulture.Name;
return systemCulture switch
{
"zh-CN" => SupportedLanguage.ZhCN,
"zh-TW" or "zh-HK" => SupportedLanguage.ZhTW,
"en-US" or "en" => SupportedLanguage.EnUS,
"ja-JP" or "ja" => SupportedLanguage.JaJP, // 新增 | New
"ko-KR" or "ko" => SupportedLanguage.KoKR, // 新增 | New
_ when systemCulture.StartsWith("zh") => SupportedLanguage.ZhCN,
_ when systemCulture.StartsWith("en") => SupportedLanguage.EnUS,
_ when systemCulture.StartsWith("ja") => SupportedLanguage.JaJP, // 新增 | New
_ when systemCulture.StartsWith("ko") => SupportedLanguage.KoKR, // 新增 | New
_ => SupportedLanguage.ZhCN
};
}
catch (Exception ex)
{
_logger.Error("Failed to get system default language", ex);
return SupportedLanguage.ZhCN;
}
}
```
### 步骤 6: 测试新语言 | Step 6: Test New Language
1. 编译项目确保无错误
2. 运行应用程序
3. 在语言切换器中选择新语言
4. 重启应用程序验证新语言显示正确
---
## 常见问题 | FAQ
### Q1: 为什么语言切换后界面没有更新?
**A:** 语言切换需要重启应用程序才能生效。这是设计决策,避免了复杂的运行时 UI 刷新机制。
解决方案:
- 确保在语言切换后提示用户重启应用
- 语言设置已保存到 App.config,重启后自动加载
### Q2: 资源键不存在时会发生什么?
**A:** 系统会返回资源键本身作为回退值,并记录警告日志。
```csharp
// 如果 "NonExistent_Key" 不存在
var text = _localizationService.GetString("NonExistent_Key");
// 返回: "NonExistent_Key"
// 日志: Warning - Resource key 'NonExistent_Key' not found
```
### Q3: 如何在设计时预览本地化文本?
**A:** LocalizationExtension 在设计时会返回 `[ResourceKey]` 格式的文本。
```xml
```
### Q4: 可以在运行时动态添加资源吗?
**A:** 单个 Resx 资源文件在编译时嵌入程序集,运行时只读。支持在运行时动态注册/注销模块级资源源(`RegisterResourceSource` / `UnregisterResourceSource`),实现模块级别的资源扩展。
如需完全动态的资源(如用户自定义翻译),考虑:
- 使用数据库存储翻译
- 实现自定义 ILocalizationService
- 使用外部 JSON/XML 配置文件
### Q5: 如何处理带参数的本地化字符串?
**A:** 使用 `string.Format` 或字符串插值:
```csharp
// 资源文件中定义
// Welcome_User = "欢迎,{0}!"
var userName = "张三";
var message = string.Format(
_localizationService.GetString("Welcome_User"),
userName);
// 结果: "欢迎,张三!"
```
### Q6: 多个模块如何共享本地化资源?
**A:** V1.1 版本引入了多资源源 Fallback Chain 机制,各模块可以注册自己的 Resx 资源文件。
所有模块通过依赖注入获取 `ILocalizationService`,共享统一的查找入口。各模块在 Prism 模块初始化时注册自己的 `ResourceManager`:
```csharp
// 在模块的 OnInitialized 中注册 | Register in module's OnInitialized
public void OnInitialized(IContainerProvider containerProvider)
{
var localizationService = containerProvider.Resolve();
var resourceManager = new ResourceManager(
"XP.Scan.Resources.Resources",
typeof(ScanModule).Assembly);
localizationService.RegisterResourceSource("XP.Scan", resourceManager);
}
```
查找时从最后注册的资源源开始向前遍历,第一个返回非 null 值的即为结果。`XP.Common` 作为默认资源源始终兜底。
通过命名约定区分资源键(如 `Scan_Button_Start`、`RaySource_StartSuccess`)仍然是推荐的最佳实践。
### Q7: 如何确保所有语言的资源键一致?
**A:** 建议使用工具或脚本验证:
```csharp
// 验证资源完整性的示例代码
public class ResourceValidator
{
public void ValidateResources()
{
var defaultKeys = GetResourceKeys("Resources.resx");
var zhTwKeys = GetResourceKeys("Resources.zh-TW.resx");
var enUsKeys = GetResourceKeys("Resources.en-US.resx");
var missingInZhTw = defaultKeys.Except(zhTwKeys);
var missingInEnUs = defaultKeys.Except(enUsKeys);
if (missingInZhTw.Any())
{
Console.WriteLine($"Missing in zh-TW: {string.Join(", ", missingInZhTw)}");
}
if (missingInEnUs.Any())
{
Console.WriteLine($"Missing in en-US: {string.Join(", ", missingInEnUs)}");
}
}
private HashSet GetResourceKeys(string resourceFile)
{
// 实现资源键提取逻辑
// Implementation for extracting resource keys
return new HashSet();
}
}
```
---
## 故障排除 | Troubleshooting
### 问题 1: LocalizationExtension 未找到
**症状:**
```
The type 'loc:Localization' was not found.
```
**解决方案:**
1. 检查命名空间声明是否正确
2. 确认程序集名称为 `XP.Common`
3. 清理并重新编译解决方案
```xml
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
```
### 问题 2: 语言设置未保存
**症状:** 重启应用后语言恢复为默认值
**解决方案:**
1. 检查 App.config 文件是否存在
2. 确认应用程序有写入配置文件的权限
3. 查看日志中的错误信息
```csharp
// 检查配置文件路径
var configPath = ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.None).FilePath;
Console.WriteLine($"Config file: {configPath}");
```
### 问题 3: 资源文件未嵌入程序集
**症状:** 运行时找不到资源
**解决方案:**
1. 检查 .csproj 文件中的资源文件配置
2. 确认 Build Action 设置为 `Embedded Resource`
3. 确认 Custom Tool 设置为 `ResXFileCodeGenerator`(仅 Resources.resx)
```xml
ResXFileCodeGenerator
Resources.Designer.cs
```
### 问题 4: ILocalizationService 未注册
**症状:**
```
Unable to resolve type 'ILocalizationService'
```
**解决方案:**
1. 确认 CommonModule 已在 App.xaml.cs 中注册
2. 检查 CommonModule.RegisterTypes 方法中的服务注册
```csharp
// App.xaml.cs
protected override void ConfigureModuleCatalog(IModuleCatalog moduleCatalog)
{
moduleCatalog.AddModule();
// 其他模块...
}
// CommonModule.cs
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton();
containerRegistry.RegisterSingleton();
}
```
### 问题 5: 设计时显示 [ResourceKey]
**症状:** XAML 设计器中显示 `[App_Title]` 而不是实际文本
**解决方案:** 这是正常行为。LocalizationExtension 在设计时返回资源键格式,运行时返回实际翻译。
如需设计时预览,可以使用 `d:DataContext` 提供设计时数据:
```xml
```
---
## 性能优化建议 | Performance Optimization Tips
### 1. 避免频繁调用 GetString
**不推荐:**
```csharp
// 每次属性访问都调用 GetString
public string Title => _localizationService.GetString("App_Title");
```
**推荐:**
```csharp
// 缓存翻译结果
private string _title;
public string Title
{
get => _title ??= _localizationService.GetString("App_Title");
}
// 语言切换时刷新缓存
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
_title = null;
RaisePropertyChanged(nameof(Title));
}
```
### 2. 使用 XAML 标记扩展而非代码
**不推荐:**
```csharp
// 在 ViewModel 中设置文本
public string ButtonText => _localizationService.GetString("Button_OK");
```
**推荐:**
```xml
```
### 3. 批量更新 UI
**不推荐:**
```csharp
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
RaisePropertyChanged(nameof(Title));
RaisePropertyChanged(nameof(Description));
RaisePropertyChanged(nameof(Status));
// ... 更多属性
}
```
**推荐:**
```csharp
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
// 使用空字符串通知所有属性变更
RaisePropertyChanged(string.Empty);
}
```
---
## 最佳实践 | Best Practices
### 1. 资源键命名约定
```
模块_功能_类型
Module_Feature_Type
示例 | Examples:
- RaySource_StartSuccess (射线源模块 - 启动成功)
- Detector_ConnectionFailed (探测器模块 - 连接失败)
- Common_Button_OK (通用 - 按钮 - 确定)
- Validation_UsernameRequired (验证 - 用户名必填)
```
### 2. 保持资源文件同步
- 添加新资源键时,同时更新所有语言的资源文件
- 使用版本控制跟踪资源文件变更
- 定期运行资源完整性验证
### 3. 提供上下文注释
```xml
保存
通用保存按钮文本 | Generic save button text
```
### 4. 避免硬编码文本
**不推荐:**
```csharp
MessageBox.Show("操作成功", "提示");
```
**推荐:**
```csharp
var message = _localizationService.GetString("Message_OperationSuccess");
var title = _localizationService.GetString("Title_Notice");
MessageBox.Show(message, title);
```
### 5. 使用有意义的资源键
**不推荐:**
```
Text1, Text2, Label3
```
**推荐:**
```
App_Title, Menu_File, Button_OK
```
---
## 模块化本地化(Modular Localization)| v2.0 新增
### 背景 | Background
v1.0 中所有多语言资源集中在 `XP.Common/Resources/Resources.resx`。随着子项目增多(XP.Scan、XP.Detector 等),集中式方案导致:
- resx 文件过大,维护困难
- 多人协作容易 Git 冲突
- 模块间资源键混在一起,职责不清
v2.0 引入 **Fallback Chain** 机制,每个子项目可以维护自己的 resx 资源文件,查找时先查模块自己的 resx,找不到再 fallback 到 XP.Common 的公共 resx。
### 架构概览 | Architecture Overview
```
查找顺序(后注册 = 高优先级):
GetString("Scan_Button_Start")
→ [1] XP.Scan/Resources/Resources.resx ← 先查模块资源(找到,返回)
→ [0] XP.Common/Resources/Resources.resx ← 找不到才 fallback
GetString("App_Title")
→ [1] XP.Scan/Resources/Resources.resx ← 模块里没有这个 key
→ [0] XP.Common/Resources/Resources.resx ← fallback 到公共资源(找到,返回)
```
### 资源文件分层规范 | Resource File Layering
| 层级 | 位置 | 内容 | 示例 |
|------|------|------|------|
| 公共层 | `XP.Common/Resources/` | 通用文案(按钮、状态、对话框等) | `App_Title`, `Button_OK`, `Status_Ready` |
| 模块层 | `XP.Scan/Resources/` | 模块专属文案 | `Scan_Button_Start`, `Scan_Text_ScanMode` |
| 模块层 | `XP.Detector/Resources/` | 模块专属文案 | `Detector_ConnectButton` |
### 新增接口方法 | New Interface Methods
`ILocalizationService` 新增两个方法:
```csharp
///
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
///
void RegisterResourceSource(string name, ResourceManager resourceManager);
///
/// 从 Fallback Chain 中注销指定资源源
///
void UnregisterResourceSource(string name);
```
### 子项目接入步骤 | Module Integration Steps
以 XP.Scan 为例,完整接入只需 3 步:
#### 步骤 1:创建模块 resx 文件
在子项目下创建 `Resources/` 目录,添加 4 个 resx 文件:
```
XP.Scan/
Resources/
Resources.resx ← 默认语言(zh-CN)
Resources.zh-CN.resx ← 简体中文
Resources.en-US.resx ← 英文
Resources.zh-TW.resx ← 繁体中文
```
#### 步骤 2:配置 csproj
在 `XP.Scan.csproj` 中添加嵌入资源配置:
```xml
PublicResXFileCodeGenerator
Resources.Designer.cs
Resources.resx
Resources.resx
Resources.resx
True
True
Resources.resx
```
#### 步骤 3:在 App 启动时注册资源源
在 `App.xaml.cs` 的 `CreateShell()` 中注册模块的 ResourceManager:
```csharp
protected override Window CreateShell()
{
var localizationService = Container.Resolve();
LocalizationExtension.Initialize(localizationService);
LocalizationHelper.Initialize(localizationService);
// 注册模块资源源
var scanResourceManager = new ResourceManager(
"XP.Scan.Resources.Resources", typeof(App).Assembly);
localizationService.RegisterResourceSource("XP.Scan", scanResourceManager);
return Container.Resolve();
}
```
注意:`ResourceManager` 的第一个参数是嵌入资源的全名,格式为 `{RootNamespace}.Resources.Resources`。如果 csproj 没有显式设置 ``,默认使用程序集名称。
### 资源键命名约定 | Resource Key Naming Convention
格式:`模块名_功能_类型`
| 模块 | 前缀 | 示例 |
|------|------|------|
| XP.Common(公共) | `App_`, `Button_`, `Status_`, `Menu_`, `Dialog_` | `App_Title`, `Button_OK` |
| XP.Scan | `Scan_` | `Scan_Button_Start`, `Scan_Text_ScanMode` |
| XP.Detector | `Detector_` | `Detector_ConnectButton` |
| XP.RaySource | `RaySource_` | `RaySource_TurnOnButton` |
规则:
- 公共文案(多个模块共用的按钮、状态等)放在 `XP.Common/Resources/` 中
- 模块专属文案放在各自项目的 `Resources/` 中
- 模块资源键必须以模块名为前缀,避免跨模块冲突
- 如果模块 resx 中定义了与 XP.Common 同名的 key,模块的值会覆盖公共值
### XAML 用法(无变化)| XAML Usage (No Change)
XAML 中的 `{loc:Localization}` 标记扩展用法完全不变,它通过 `ILocalizationService.GetString()` 查找,自动走 Fallback Chain:
```xml
```
### LocalizationHelper 用法(无变化)| LocalizationHelper Usage (No Change)
`LocalizationHelper.Get()` 也自动走 Fallback Chain,用法不变:
```csharp
// 自动从 Fallback Chain 查找
var text = LocalizationHelper.Get("Scan_Button_Start");
```
前提是在 App 启动时调用过 `LocalizationHelper.Initialize(localizationService)`。
### 线程安全 | Thread Safety
Fallback Chain 使用 `ReaderWriterLockSlim` 保护:
- `GetString` 获取读锁(多线程可并发读)
- `RegisterResourceSource` / `UnregisterResourceSource` 获取写锁(互斥)
- 读写不互斥时性能优于 `lock`
### 错误处理 | Error Handling
| 场景 | 行为 |
|------|------|
| `RegisterResourceSource` 传入 null | 抛出 `ArgumentNullException` |
| 重复注册同名资源源 | 抛出 `InvalidOperationException` + 警告日志 |
| 注销不存在的资源源 | 静默忽略 + 警告日志 |
| 注销默认资源源 `"XP.Common"` | 抛出 `InvalidOperationException`(禁止) |
| 所有资源源都找不到 key | 返回 key 本身 + 警告日志 |
| 单个 ResourceManager 抛异常 | 捕获并继续查找下一个 |
### 涉及的代码文件 | Modified Files
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `XP.Common/Localization/Interfaces/ILocalizationService.cs` | 修改 | 新增 `RegisterResourceSource` / `UnregisterResourceSource` |
| `XP.Common/Localization/Implementations/ResourceSource.cs` | 新增 | 资源源内部模型(name + ResourceManager) |
| `XP.Common/Localization/Implementations/ResxLocalizationService.cs` | 修改 | Fallback Chain + ReaderWriterLockSlim |
| `XP.Common/Localization/Helpers/LocalizationHelper.cs` | 修改 | 委托给 ILocalizationService + Initialize 方法 |
### 向后兼容性 | Backward Compatibility
- 未注册任何模块资源源时,行为与 v1.0 完全一致(仅使用 XP.Common 的 resx)
- 现有 XAML 中的 `{loc:Localization}` 无需修改
- 现有代码中的 `ILocalizationService.GetString()` 无需修改
- 现有代码中的 `LocalizationHelper.Get()` 无需修改
---
## 相关文档 | Related Documentation
---
## 技术支持 | Technical Support
如有问题或建议,请联系开发团队或查看项目文档。
For questions or suggestions, please contact the development team or refer to the project documentation.
---
**版本 | Version:** 2.0
**最后更新 | Last Updated:** 2026-04-01