Skip to content

使用 MarkupExtension 自定义多语言的处理

Published: at 11:00

这里介绍一种在 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 声明都默认的命名空间中去。

让你编写的控件库在 XAML 中有一个统一的漂亮的命名空间(xmlns)和命名空间前缀 - walterlv

你需要一个单独定义一个程序集,声明

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 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。