36 KiB
多语言支持使用指南 | 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
<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 文件顶部添加命名空间引用:
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
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 多资源源查找。
// 在 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
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
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
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
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
<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
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
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 中注册:
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
// 注销指定资源源 | 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:
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
- 在
XplorePlane.Common/Resources/目录下创建新的资源文件 - 文件命名格式:
Resources.<culture-code>.resx - 例如:
Resources.ja-JP.resx、Resources.ko-KR.resx
步骤 3: 添加翻译内容 | Step 3: Add Translations
在新资源文件中添加所有资源键的翻译:
<?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 方法:
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 方法:
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
- 编译项目确保无错误
- 运行应用程序
- 在语言切换器中选择新语言
- 重启应用程序验证新语言显示正确
常见问题 | FAQ
Q1: 为什么语言切换后界面没有更新?
A: 语言切换需要重启应用程序才能生效。这是设计决策,避免了复杂的运行时 UI 刷新机制。
解决方案:
- 确保在语言切换后提示用户重启应用
- 语言设置已保存到 App.config,重启后自动加载
Q2: 资源键不存在时会发生什么?
A: 系统会返回资源键本身作为回退值,并记录警告日志。
// 如果 "NonExistent_Key" 不存在
var text = _localizationService.GetString("NonExistent_Key");
// 返回: "NonExistent_Key"
// 日志: Warning - Resource key 'NonExistent_Key' not found
Q3: 如何在设计时预览本地化文本?
A: LocalizationExtension 在设计时会返回 [ResourceKey] 格式的文本。
<!-- 设计时显示: [App_Title] -->
<!-- 运行时显示: XplorePlane X射线检测系统 -->
<TextBlock Text="{loc:Localization Key=App_Title}" />
Q4: 可以在运行时动态添加资源吗?
A: 单个 Resx 资源文件在编译时嵌入程序集,运行时只读。支持在运行时动态注册/注销模块级资源源(RegisterResourceSource / UnregisterResourceSource),实现模块级别的资源扩展。
如需完全动态的资源(如用户自定义翻译),考虑:
- 使用数据库存储翻译
- 实现自定义 ILocalizationService
- 使用外部 JSON/XML 配置文件
Q5: 如何处理带参数的本地化字符串?
A: 使用 string.Format 或字符串插值:
// 资源文件中定义
// Welcome_User = "欢迎,{0}!"
var userName = "张三";
var message = string.Format(
_localizationService.GetString("Welcome_User"),
userName);
// 结果: "欢迎,张三!"
Q6: 多个模块如何共享本地化资源?
A: V1.1 版本引入了多资源源 Fallback Chain 机制,各模块可以注册自己的 Resx 资源文件。
所有模块通过依赖注入获取 ILocalizationService,共享统一的查找入口。各模块在 Prism 模块初始化时注册自己的 ResourceManager:
// 在模块的 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: 建议使用工具或脚本验证:
// 验证资源完整性的示例代码
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.
解决方案:
- 检查命名空间声明是否正确
- 确认程序集名称为
XP.Common - 清理并重新编译解决方案
<!-- 正确的命名空间声明 | Correct namespace declaration -->
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
问题 2: 语言设置未保存
症状: 重启应用后语言恢复为默认值
解决方案:
- 检查 App.config 文件是否存在
- 确认应用程序有写入配置文件的权限
- 查看日志中的错误信息
// 检查配置文件路径
var configPath = ConfigurationManager.OpenExeConfiguration(
ConfigurationUserLevel.None).FilePath;
Console.WriteLine($"Config file: {configPath}");
问题 3: 资源文件未嵌入程序集
症状: 运行时找不到资源
解决方案:
- 检查 .csproj 文件中的资源文件配置
- 确认 Build Action 设置为
Embedded Resource - 确认 Custom Tool 设置为
ResXFileCodeGenerator(仅 Resources.resx)
<!-- .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'
解决方案:
- 确认 CommonModule 已在 App.xaml.cs 中注册
- 检查 CommonModule.RegisterTypes 方法中的服务注册
// 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 提供设计时数据:
<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
不推荐:
// 每次属性访问都调用 GetString
public string Title => _localizationService.GetString("App_Title");
推荐:
// 缓存翻译结果
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 标记扩展而非代码
不推荐:
// 在 ViewModel 中设置文本
public string ButtonText => _localizationService.GetString("Button_OK");
推荐:
<!-- 直接在 XAML 中绑定 -->
<Button Content="{loc:Localization Key=Button_OK}" />
3. 批量更新 UI
不推荐:
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
{
RaisePropertyChanged(nameof(Title));
RaisePropertyChanged(nameof(Description));
RaisePropertyChanged(nameof(Status));
// ... 更多属性
}
推荐:
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. 提供上下文注释
<data name="Button_Save" xml:space="preserve">
<value>保存</value>
<comment>通用保存按钮文本 | Generic save button text</comment>
</data>
4. 避免硬编码文本
不推荐:
MessageBox.Show("操作成功", "提示");
推荐:
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 新增两个方法:
/// <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 中添加嵌入资源配置:
<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:
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:
<!-- 查找 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,用法不变:
// 自动从 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