使用 MarkupExtension 自定义多语言的处理
这里介绍一种在 WPF 中实现多语言本地化的方式,使用 MarkupExtension 从任何自定义的多语言获取方式中读取具体的语言项。
多语言 Provider
这里使用的是 dotnet-campus/dotnetCampus.YamlToCSharp: 将 YAML 文件转 C# 代码
将多语言定义到 yaml 中,然后通过 C# 字典的形式获取。
MarkupExtension
定义一个 LangExtension,然后就可以直接在 xaml 中使用了
public class LangExtension(string key) : MarkupExtension
{
    private string Key { get; } = key;
    public override object? ProvideValue(IServiceProvider serviceProvider)
    {
        return GetLocalizedValue(Key);
    }
    private string GetLocalizedValue(string key)
    {
        return AppContainer.LocalizationProvider.GetLang(key);
    }
}
<Window x:Class="LocalizationWpfDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:LocalizationWpfDemo"
        xmlns:l="clr-namespace:LocalizationWpfDemo.Localization"
        mc:Ignorable="d"
        Title="{l:Lang Title}" Height="450" Width="800">
    <Grid Margin="8">
        <TextBlock Grid.Row="1" Text="{l:Lang Message}" HorizontalAlignment="Center" VerticalAlignment="Center"></TextBlock>
    </Grid>
</Window>
注意,这里使用 xmlns:l="clr-namespace:LocalizationWpfDemo.Localization" 声明 LangExtension 所在的命名空间,
使用时要带上命名空间 Text="{l:Lang Message}"。
如果想要使用 Text="{Lang Message}" 这样看起来更优雅的方式,也省去每个 xaml 都要声明命名空间的烦恼,可以将 LangExtension 声明都默认的命名空间中去。
你需要一个单独定义一个程序集,声明
using System.Windows.Markup;
// 标记此程序集的命名空间,加入到 WPF 默认的 XAML 命名空间中,
// 这样就可以在 XAML 中直接使用此程序集命名空间下的类了,而不需要在 XAML 中引用命名空间
[assembly: XmlnsDefinition(
    "http://schemas.microsoft.com/winfx/2006/xaml/presentation",
    "LocalizationWpfDemo.Localization"  // 这里替换成 LangExtension 所在的命名空间
)]
然后就可以通过 Text="{Lang Message}" 这样的方式在 xaml 中使用多语言了。
语言项不能动态更新
上面的 LangExtension 实现,返回的是一个字符串类型,返回之后就“固定”在界面上了,如果需要切换多重不同语言,只能重启程序,在 LocalizationProvider 中根据配置,返回另一种语言的内容才行。
如何实现不重启程序的动态语言切换?
答案:在 LangExtension 不返回 string 类型,而是返回 Binding。
当然,这里需要同步修改 LocalizationProvider,让 LocalizationProvider 可以具体通知语言项变更的能力。
以下是 LocalizationProvider 的源码
重点是继承 INotifyPropertyChanged 接口,并在 CurrentLanguage 修改之后,能够触发 PropertyChanged 事件即可。
public sealed class LocalizationProvider : INotifyPropertyChanged
{
    private string _initLanguage;
    private IDictionary<string, string> _localizedValues;
    private readonly Dictionary<string, IYamlCSharpDictionary[]> _allLang;
    public event PropertyChangedEventHandler? PropertyChanged;
    public string CurrentLanguage
    {
        get => _initLanguage;
        set
        {
            Update(value);
            SetField(ref _initLanguage, value);
        }
    }
    public string this[string key] => GetLang(key);
    
    public LocalizationProvider(string initLanguage)
    {
        _initLanguage = initLanguage;
        _allLang = new Dictionary<string, IYamlCSharpDictionary[]>
        {
            { "en_US", [new en_US.Main()] },
            { "zh_CN", [new zh_CN.Main()] },
        };
        _localizedValues = _allLang[initLanguage].SelectMany(x => x.AsDictionary()).ToDictionary();
    }
    public string GetLang(string key)
    {
        key = "Lang." + key;
        if (_localizedValues.TryGetValue(key, out var value))
        {
            return value;
        }
        return "";
    }
    private void Update(string lang)
    {
        _localizedValues = _allLang[lang].SelectMany(x => x.AsDictionary()).ToDictionary();
    }
    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}
新的 LangExtension 实现修改成
public class LangExtension(string key) : MarkupExtension
{
    private string Key { get; } = key;
    public override object? ProvideValue(IServiceProvider serviceProvider)
    {
        // return GetLocalizedValue(Key);
        var binding = new Binding(nameof(LocalizationProvider.CurrentLanguage))
        {
            Mode = BindingMode.OneWay,
            Source = AppContainer.LocalizationProvider,
            UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged,
            Converter = new LangConverter(Key)
        };
        return binding.ProvideValue(serviceProvider);
    }
    // private string GetLocalizedValue(string key)
    // {
    //     return AppContainer.LocalizationProvider.GetLang(key);
    // }
}
public class LangConverter(string key) : IValueConverter
{
    private string Key { get; } = key;
    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        return AppContainer.LocalizationProvider.GetLang(Key);
    }
    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}
这里的重点是返回 Binding,绑定到 LocalizationProvider 的 CurrentLanguage 属性上,如果 CurrentLanguage 属性变化,就可以立即返回新的值。
案例源码
https://gitee.com/Jasongrass/DemoPark/tree/master/Code/LocalizationWpfDemo


 
                
            
         浙公网安备 33010602011771号
浙公网安备 33010602011771号