Files
XplorePlane/XP.Common/Documents/Localization.Guidance.md
T

1177 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 多语言支持使用指南 | 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
<Window x:Class="XplorePlane.App.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
Title="{loc:Localization Key=App_Title}">
<Grid>
<!-- 按钮文本本地化 | Localized button text -->
<Button Content="{loc:Localization Key=Button_OK}" />
<!-- 标签文本本地化 | Localized label text -->
<Label Content="{loc:Localization Key=Settings_Language}" />
<!-- 菜单项本地化 | Localized menu item -->
<MenuItem Header="{loc:Localization Key=Menu_File}" />
</Grid>
</Window>
```
#### 命名空间声明 | 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<LanguageOption> 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
<UserControl x:Class="XplorePlane.App.Views.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common">
<StackPanel Margin="20">
<!-- 语言选择标签 | Language selection label -->
<Label Content="{loc:Localization Key=Settings_Language}"
FontWeight="Bold"
Margin="0,0,0,10"/>
<!-- 语言下拉框 | Language combo box -->
<ComboBox ItemsSource="{Binding AvailableLanguages}"
SelectedValue="{Binding SelectedLanguage}"
SelectedValuePath="Language"
DisplayMemberPath="DisplayName"
Width="200"
HorizontalAlignment="Left">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Flag}" Margin="0,0,8,0"/>
<TextBlock Text="{Binding DisplayName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- 提示信息 | Hint message -->
<TextBlock Text="{loc:Localization Key=Settings_LanguageHint}"
Margin="0,10,0,0"
Foreground="Gray"
TextWrapping="Wrap"/>
</StackPanel>
</UserControl>
```
---
### 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<LanguageChangedEvent>()
.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.<culture-code>.resx`
3. 例如:`Resources.ja-JP.resx``Resources.ko-KR.resx`
### 步骤 3: 添加翻译内容 | Step 3: Add Translations
在新资源文件中添加所有资源键的翻译:
```xml
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="App_Title" xml:space="preserve">
<value>XplorePlane X線検査システム</value>
</data>
<data name="Menu_File" xml:space="preserve">
<value>ファイル</value>
</data>
<!-- 添加所有其他资源键 | Add all other resource keys -->
</root>
```
### 步骤 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
<!-- 设计时显示: [App_Title] -->
<!-- 运行时显示: XplorePlane X射线检测系统 -->
<TextBlock Text="{loc:Localization Key=App_Title}" />
```
### 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<ILocalizationService>();
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<string> GetResourceKeys(string resourceFile)
{
// 实现资源键提取逻辑
// Implementation for extracting resource keys
return new HashSet<string>();
}
}
```
---
## 故障排除 | Troubleshooting
### 问题 1: LocalizationExtension 未找到
**症状:**
```
The type 'loc:Localization' was not found.
```
**解决方案:**
1. 检查命名空间声明是否正确
2. 确认程序集名称为 `XP.Common`
3. 清理并重新编译解决方案
```xml
<!-- 正确的命名空间声明 | Correct namespace declaration -->
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
<!-- .csproj 中的正确配置 | Correct configuration in .csproj -->
<ItemGroup>
<EmbeddedResource Include="Resources\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Include="Resources\Resources.zh-CN.resx" />
<EmbeddedResource Include="Resources\Resources.zh-TW.resx" />
<EmbeddedResource Include="Resources\Resources.en-US.resx" />
</ItemGroup>
```
### 问题 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>();
// 其他模块...
}
// CommonModule.cs
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<ILocalizationConfig, LocalizationConfig>();
containerRegistry.RegisterSingleton<ILocalizationService, ResxLocalizationService>();
}
```
### 问题 5: 设计时显示 [ResourceKey]
**症状:** XAML 设计器中显示 `[App_Title]` 而不是实际文本
**解决方案:** 这是正常行为。LocalizationExtension 在设计时返回资源键格式,运行时返回实际翻译。
如需设计时预览,可以使用 `d:DataContext` 提供设计时数据:
```xml
<Window xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel, IsDesignTimeCreatable=True}">
<!-- 内容 | Content -->
</Window>
```
---
## 性能优化建议 | 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
<!-- 直接在 XAML 中绑定 -->
<Button Content="{loc:Localization Key=Button_OK}" />
```
### 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
<data name="Button_Save" xml:space="preserve">
<value>保存</value>
<comment>通用保存按钮文本 | Generic save button text</comment>
</data>
```
### 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
/// <summary>
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
/// </summary>
void RegisterResourceSource(string name, ResourceManager resourceManager);
/// <summary>
/// 从 Fallback Chain 中注销指定资源源
/// </summary>
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
<ItemGroup>
<EmbeddedResource Update="Resources\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.zh-CN.resx">
<DependentUpon>Resources.resx</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.zh-TW.resx">
<DependentUpon>Resources.resx</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Resources.en-US.resx">
<DependentUpon>Resources.resx</DependentUpon>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
```
#### 步骤 3:在 App 启动时注册资源源
`App.xaml.cs``CreateShell()` 中注册模块的 ResourceManager
```csharp
protected override Window CreateShell()
{
var localizationService = Container.Resolve<ILocalizationService>();
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<MainWindow>();
}
```
注意:`ResourceManager` 的第一个参数是嵌入资源的全名,格式为 `{RootNamespace}.Resources.Resources`。如果 csproj 没有显式设置 `<RootNamespace>`,默认使用程序集名称。
### 资源键命名约定 | 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
<!-- 查找 XP.Scan 的 resx,找到返回 -->
<TextBlock Text="{loc:Localization Scan_Text_ScanMode}" />
<!-- 查找 XP.Scan 的 resx 没有,fallback 到 XP.Common 的 resx -->
<TextBlock Text="{loc:Localization App_Title}" />
```
### 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