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

36 KiB
Raw Blame History

多语言支持使用指南 | 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

  1. XplorePlane.Common/Resources/ 目录下创建新的资源文件
  2. 文件命名格式:Resources.<culture-code>.resx
  3. 例如:Resources.ja-JP.resxResources.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

  1. 编译项目确保无错误
  2. 运行应用程序
  3. 在语言切换器中选择新语言
  4. 重启应用程序验证新语言显示正确

常见问题 | 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_StartRaySource_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.

解决方案:

  1. 检查命名空间声明是否正确
  2. 确认程序集名称为 XP.Common
  3. 清理并重新编译解决方案
<!-- 正确的命名空间声明 | Correct namespace declaration -->
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"

问题 2: 语言设置未保存

症状: 重启应用后语言恢复为默认值

解决方案:

  1. 检查 App.config 文件是否存在
  2. 确认应用程序有写入配置文件的权限
  3. 查看日志中的错误信息
// 检查配置文件路径
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
<!-- .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 方法中的服务注册
// 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.csCreateShell() 中注册模块的 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() 无需修改


技术支持 | 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