这里介绍一种在 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
原文链接: https://blog.jgrass.cc/posts/wpf-localization-binding/
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。