语言:C#
IDE:Microsoft Visual Studio Community 2022
框架:WPF,.net 8.0
插件:ResXManager,CommunityToolkit.Mvvm
目录
一、说明
本文是自己在探索WPF多语言时的总结,主要有以下三种方式:
1.使用locBaml,此方式比较繁琐。
2.使用资源字典文件,此方式相对简单。
3.使用.resx资源文件,采用ResXManager插件可以很便捷的进行开发。
插件安装方式查看
[Visual Stuidio 2022使用技巧]1.插件及风格-CSDN博客
本地化需要处理的情况,个人总结有3种:
1.1 xaml文件中的静态内容
如TextBlock的Text,Button的Content等
<TextBlock Text="用户名"/>
<Button Content="登录"/>
1.2 后台文件中的内容
.cs文件中的内容,如:
[Required(ErrorMessage ="用户名不能为空")]
string name;
xaml文件中的绑定内容也归入此类
1.3 系统控件中的内容
如MessageBox的确定按钮
二、 准备工作
2.1 Demo准备
先准备一个只含中文的Demo:
界面:
功能:
1.需要输入三个字段,用户名,手机号,年龄
2.每个字段有一个验证器,验证失败的时候界面会提示错误信息,点击登录提示登录失败
3.全部验证成功后点击登录提示登录成功
4.点击中文后页面语言切换成中文,点击英文后页面语言切换成英文
说明:通过CommunityToolkit.Mvvm插件的ObservableValidator实现,参看:
[CommunityToolkit.Mvvm个人总结]3.Validator-CSDN博客
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.ComponentModel.DataAnnotations;
using System.Windows;
namespace LocalizationDemo.ViewModels
{
public partial class MainViewModel : ObservableValidator
{
[ObservableProperty]
[Required(ErrorMessage ="用户名不能为空")]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
string name;
[ObservableProperty]
[Required(ErrorMessage = "手机号不能为空")]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
string phone;
[ObservableProperty]
[Range(18, 150,ErrorMessage ="年龄范围18-150")]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
int age;
public string ErrorMessages
{
get
{
return UpdateErrorMessage();
}
}
string UpdateErrorMessage()
{
if (HasErrors)
{
return string.Join(Environment.NewLine, GetErrors());
}
else
{
return string.Empty;
}
}
[RelayCommand]
void LoginClick()
{
ValidateAllProperties();
if (HasErrors)
{
MessageBox.Show("注册失败");
}
else
{
MessageBox.Show("注册成功");
}
}
[RelayCommand]
void ToChinese()
{
}
[RelayCommand]
void ToEnglish()
{
}
public MainViewModel()
{
ValidateAllProperties();
}
}
}
View:
<Window x:Class="LocalizationDemo.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:LocalizationDemo.ViewModels"
Width="600"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="5" />
</Style>
<Style TargetType="TextBox">
<Setter Property="Margin" Value="5" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
</Style>
</Window.Resources>
<Grid>
<StackPanel Margin="10" TextElement.FontSize="32">
<TextBlock Text="用户名" />
<TextBox Text="{Binding Name, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="手机号" />
<TextBox Text="{Binding Phone, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="年龄" />
<TextBox Text="{Binding Age, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Separator Width="500" />
<TextBlock Text="{Binding ErrorMessages}" Foreground="Red" />
<Button Content="登录" Command="{Binding LoginClickCommand}" />
<Button Content="中文" Command="{Binding ToChineseCommand}" />
<Button Content="英文" Command="{Binding ToEnglishCommand}" />
</StackPanel>
</Grid>
</Window>
2.2 插件准备
安装Resx Manager插件,安装成功后,点击工具-Resx Manager,配置默认语言
2.3 添加资源文件
Properties-右键-添加-新建项
搜索资源文件,添加Lang.resx的资源文件
使用Resx Manager打开资源文件,有两种方式:
1.在资源文件的右键菜单中点击Resx Manager
2.工具-Resx Manager 的左侧树形栏中选择资源文件
2.4 添加语言
添加en-US语言
2.5 添加语言切换助手
using System;
using System.ComponentModel;
using System.Globalization;
using System.Resources;
namespace LocalizationDemo
{
public class LanguageManager : INotifyPropertyChanged
{
/// <summary>
/// 资源
/// </summary>
private readonly ResourceManager resourceManager;
/// <summary>
/// 懒加载
/// </summary>
private static readonly Lazy<LanguageManager> _lazy = new(() => new LanguageManager());
public static LanguageManager Instance => _lazy.Value;
public event PropertyChangedEventHandler PropertyChanged;
public LanguageManager()
{
//获取此命名空间下Resources的Lang的资源,Lang可以修改
resourceManager = new ResourceManager("LocalizationDemo.Properties.Lang", typeof(LanguageManager).Assembly);
}
/// <summary>
/// 索引器的写法,传入字符串的下标
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public string this[string name]
{
get
{
ArgumentNullException.ThrowIfNull(name);
return resourceManager.GetString(name);
}
}
public void ChangeLanguage(CultureInfo cultureInfo)
{
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(""));
}
}
}
2.6 完善语言切换方法
[RelayCommand]
void ToChinese()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("zh-CN"));
}
[RelayCommand]
void ToEnglish()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("en-US"));
}
三、 xaml文件静态内容
例如:对以下内容进行本地化
<TextBlock Text="用户名" />
3.1 xaml添加引用
xmlns:app="clr-namespace:LocalizationDemo"
3.2 在Resx Manager添加新资源
名称为MainView_UserName,此名称自己定义,考虑到整个App的资源都在此文件中,不能太过简单,否则容易重名。
先添加中文内容:用户名,英文内容先不管,后面统一处理
3.3 修改TextBlock属性
<TextBlock Text="{Binding [MainView_UserName], Source={x:Static app:LanguageManager.Instance}}" />
此处是通过索引器实现的,首先在类LanguageManager中实现了一个索引器,可通过name访问到资源内容
public string this[string name]
{
get
{
ArgumentNullException.ThrowIfNull(name);
return resourceManager.GetString(name);
}
}
其次在xmal中,Binding的[]表达式可以访问集合或字典的特定元素(包括索引器)
比如访问集合元素:
<TextBlock Text="{Binding MyList[0]}" />
运行后,可发现界面上的用户名已正确显示,说明已经绑定成功
3.4 替换所有静态内容
重复3.2-3.3替换所有静态内容,资源文件内容为:
View:
<StackPanel Margin="10" TextElement.FontSize="32">
<TextBlock Text="{Binding [MainView_UserName], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Name, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding [MainView_Phone], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Phone, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding [MainView_Age], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Age, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Separator Width="500" />
<TextBlock Text="{Binding ErrorMessages}" Foreground="Red" />
<Button Content="{Binding [MainView_Login], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding LoginClickCommand}" />
<Button Content="{Binding [MainView_Chinese], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding ToChineseCommand}" />
<Button Content="{Binding [MainView_English], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding ToEnglishCommand}" />
</StackPanel>
3.5 添加语言内容
这里进行英文内容的补充,有两种方法
3.5.1 翻译功能
打开ResxManager控件,点击翻译-目标-勾选语言
选择需要翻译的字段,点击开始
等待翻译结束后,校对一遍,因为自动翻译的内容可能有错误,确认后点击应用所有
再返回主要页面,可以看到所有资源都已经翻译完毕
3.5.2 导出功能
点击导出全部,保存为excel文件
打开excel文件,编辑好英文内容后,再使用导入功能,也可以完成翻译工作
3.6 验证
运行程序,点击中文和英文,可以看到静态内容已经完成了本地化功能
3.7 代码
此时的ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.ComponentModel.DataAnnotations;
using System.Windows;
namespace LocalizationDemo.ViewModels
{
public partial class MainViewModel : ObservableValidator
{
[ObservableProperty]
[Required(ErrorMessage ="用户名不能为空")]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
string name;
[ObservableProperty]
[Required(ErrorMessage = "手机号不能为空")]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
string phone;
[ObservableProperty]
[Range(18, 150,ErrorMessage ="年龄范围18-150")]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
int age;
public string ErrorMessages
{
get
{
return UpdateErrorMessage();
}
}
string UpdateErrorMessage()
{
if (HasErrors)
{
return string.Join(Environment.NewLine, GetErrors());
}
else
{
return string.Empty;
}
}
[RelayCommand]
void LoginClick()
{
ValidateAllProperties();
if (HasErrors)
{
MessageBox.Show("注册失败");
}
else
{
MessageBox.Show("注册成功");
}
}
[RelayCommand]
void ToChinese()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("zh-CN"));
}
[RelayCommand]
void ToEnglish()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("en-US"));
}
public MainViewModel()
{
ValidateAllProperties();
}
}
}
View:
<Window x:Class="LocalizationDemo.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:LocalizationDemo.ViewModels"
xmlns:app="clr-namespace:LocalizationDemo"
Width="600"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="5" />
</Style>
<Style TargetType="TextBox">
<Setter Property="Margin" Value="5" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
</Style>
</Window.Resources>
<Grid>
<StackPanel Margin="10" TextElement.FontSize="32">
<TextBlock Text="{Binding [MainView_UserName], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Name, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding [MainView_Phone], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Phone, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding [MainView_Age], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Age, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Separator Width="500" />
<TextBlock Text="{Binding ErrorMessages}" Foreground="Red" />
<Button Content="{Binding [MainView_Login], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding LoginClickCommand}" />
<Button Content="{Binding [MainView_Chinese], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding ToChineseCommand}" />
<Button Content="{Binding [MainView_English], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding ToEnglishCommand}" />
</StackPanel>
</Grid>
</Window>
四、后台文件中的内容
例如对以下内容进行本地化
[Required(ErrorMessage ="用户名不能为空")]
string name;
4.1 添加语言资源
选择整个字段:ErrorMessage = "用户名不能为空"
在右键菜单中点击移动到资源
弹出以下窗口
资源:存储的资源文件名称
名称:存储的资源字段名称
代码:自动生成的引用代码,可以根据实际场景选择
值:资源内容(即中文内容)
注释:自己添加
勾选在Resx Resource Manager中编辑新项目
点击OK后,将新建一个名称为MainViewModel_UserName_Required的资源,中文内容为:用户名不能为空,并且选中的代码:
ErrorMessage = "用户名不能为空"
也被替换成了:
ErrorMessageResourceType = typeof(Lang), ErrorMessageResourceName = nameof(Lang.MainViewModel_UserName_Required)
按照3.5添加英文内容后,运行程序,可以看到已经实现了后台文件的本地化:
重复此步骤,将ViewModel文件中的其他中文字符串也进行替换。
替换完成后按照3.5的说明添加其英文内容
此时已基本完成了本地化工作,前台的ErrorMessages在语言切换时并不会更新,但是在TextBox输入变化时会正常更新,修改OnChinese和OnEnglish方法:
[RelayCommand]
void ToChinese()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("zh-CN"));
ValidateAllProperties();
OnPropertyChanged(nameof(ErrorMessages));
}
[RelayCommand]
void ToEnglish()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("en-US"));
ValidateAllProperties();
OnPropertyChanged(nameof(ErrorMessages));
}
4.2 验证
4.3 代码
ViewModel:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LocalizationDemo.Properties;
using System;
using System.ComponentModel.DataAnnotations;
using System.Windows;
namespace LocalizationDemo.ViewModels
{
public partial class MainViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
[Required(ErrorMessageResourceType = typeof(Lang), ErrorMessageResourceName = nameof(Lang.MainViewModel_UserName_Required))]
string name;
[ObservableProperty]
[Required(ErrorMessageResourceType = typeof(Lang), ErrorMessageResourceName = nameof(Lang.MainViewModel_Phone_Required))]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
string phone;
[ObservableProperty]
[Range(18, 150,ErrorMessageResourceType = typeof(Lang), ErrorMessageResourceName = nameof(Lang.MainViewModel_Age_Range))]
[NotifyDataErrorInfo]
[NotifyPropertyChangedFor(nameof(ErrorMessages))]
int age;
public string ErrorMessages
{
get
{
return UpdateErrorMessage();
}
}
string UpdateErrorMessage()
{
if (HasErrors)
{
return string.Join(Environment.NewLine, GetErrors());
}
else
{
return string.Empty;
}
}
[RelayCommand]
void LoginClick()
{
ValidateAllProperties();
if (HasErrors)
{
MessageBox.Show(Lang.LoginClick_Failed);
}
else
{
MessageBox.Show(Lang.LoginClick_Success);
}
}
[RelayCommand]
void ToChinese()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("zh-CN"));
ValidateAllProperties();
OnPropertyChanged(nameof(ErrorMessages));
}
[RelayCommand]
void ToEnglish()
{
LanguageManager.Instance.ChangeLanguage(new System.Globalization.CultureInfo("en-US"));
ValidateAllProperties();
OnPropertyChanged(nameof(ErrorMessages));
}
public MainViewModel()
{
ValidateAllProperties();
}
}
}
View:
<Window x:Class="LocalizationDemo.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:LocalizationDemo.ViewModels"
xmlns:app="clr-namespace:LocalizationDemo"
Width="600"
SizeToContent="Height"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<Window.Resources>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="5" />
</Style>
<Style TargetType="TextBox">
<Setter Property="Margin" Value="5" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
</Style>
</Window.Resources>
<Grid>
<StackPanel Margin="10" TextElement.FontSize="32">
<TextBlock Text="{Binding [MainView_UserName], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Name, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding [MainView_Phone], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Phone, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding [MainView_Age], Source={x:Static app:LanguageManager.Instance}}" />
<TextBox Text="{Binding Age, ValidatesOnNotifyDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
<Separator Width="500" />
<TextBlock Text="{Binding ErrorMessages}" Foreground="Red" />
<Button Content="{Binding [MainView_Login], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding LoginClickCommand}" />
<Button Content="{Binding [MainView_Chinese], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding ToChineseCommand}" />
<Button Content="{Binding [MainView_English], Source={x:Static app:LanguageManager.Instance}}" Command="{Binding ToEnglishCommand}" />
</StackPanel>
</Grid>
</Window>
五、系统控件中的内容
像MessageBox,OpenFileDialog,SaveFileDialog的本地化有所不同,这些都是调用系统的Win32 API。个人总结有以下几种方法:
5.1 切换系统语言
无需修改代码,完美支持,缺点是使用系统语言使用中文时,即时APP已经切换到不同语言,但是系统控件中的文本还是中文,不会切换。
方法:开始菜单-语言设置
5.2 自定义控件
通过Win32的API接口,修改MessageBox,使得其支持多语言。暂未深入研究,参考以下链接: