1177 lines
36 KiB
Markdown
1177 lines
36 KiB
Markdown
# 多语言支持使用指南 | 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
|