构建多语言 WPF 应用程序






4.84/5 (22投票s)
概述了如何在 WPF 应用程序中构建多语言支持,其中语言可以在运行时动态更改。
目录
引言
我最近一直在进行的一项工作是为 WPF 应用程序构建多语言支持,以提高英语不好的更广泛受众的可用性。我希望在此过程中实现几个目标:
- 让一个版本的应用程序支持多种语言。这意味着没有单独的英文版、法文版、日文版等。许多电子产品(如电视和数码相机)都支持同一型号的多种语言。您不必购买略有不同的型号或应用固件补丁才能拥有与默认安装不同的语言。
- 允许在运行时轻松切换界面语言。无需关闭应用程序并配置操作系统区域设置,更不用说重新运行安装程序了。
- 首次使用时选择正确的语言。首次运行应用程序时,它会将界面语言设置为操作系统指定的系统语言。这很合理——法国用户希望立即安装、运行和使用该软件,而不是在能够使用之前尝试导航一个不熟悉的应用程序来更改语言。
- 允许 UI 可伸缩以适应翻译后的文本,最大限度地减少文本被截断的可能性。
此外,在用户界面不断增长的情况下,此实现的实现不应增加开发难度。(这一点我吃过亏。)
因此,本文旨在概述我为实现这些目标而开发的解决方案的详细信息,该解决方案基于我为此撰写的一些博客文章(在此,在此,和在此)。在此过程中,我将指出示例的相关部分,让您了解所有内容是如何协同工作的。
免责声明:示例中的文本是使用在线自动翻译服务翻译的。尽管已尽最大努力确保其准确性(通过反向翻译检查),但翻译内容可能存在一些不准确或错误。尤其是在使用我无法理解的完全不同的书写系统时。
高层概述
此实现专为遵循模型-视图-视图模型(MVVM)模式的 WPF 应用程序而设计。语言数据存储在嵌入的 XML 文件中,这些文件会在需要时(即界面语言更改时)加载到内存中。这构成了模型的一部分。
视图模型向整个 WPF 应用程序公开了一个包含当前语言语言数据的属性。视图是由 XAML 文件组成的集合,其中包含此语言数据的绑定。为了选择特定文本元素所需的精确值,每个绑定都使用带有转换器参数的自定义值转换器来执行文本键查找。最后,使用自定义标记扩展来抽象绑定细节,因此只需要指定键(即转换器参数)。
示例
为了说明此实现如何实际工作,我围绕此功能创建了一个小型示例应用程序。该应用程序被称为“RePaver”,它可以整理路径标记表达式,并增加了对实际几何图形执行基本旋转、翻转、平移和缩放的功能(即无需布局变换)。在后台,它使用正则表达式提取路径段并对每个段执行就地转换。
举个例子,考虑以下您通常从导出为 XAML 的矢量图形中获得的路径表达式示例(这与我在工作中遇到的一些路径相比不算什么!)
<Path Data="M 470.567,400.914 L 470.578,
390.903 L466.551,390.863 L 472.6,384.876 L472.598,400.888 Z" ... />
如果您将 Data 表达式(引号内的内容)复制并粘贴到“输入”框中,然后单击“Go”,您应该会看到以下输出
M 4,16 L 4,6 L 0,6 L 6,0 L 6,16 Z
您还将在右侧获得一个方便的可视化效果,“之前”和“之后”。
随意调整设置 - 您会注意到操作的顺序是旋转/翻转 -> 缩放到 [边界框大小] -> 偏移。当然,随意尝试使用不同的语言。
模型
XML
如前所述,构成用户界面的每一条文本都以本地化形式存储在每个语言的 XML 文件中,所有 XML 文件都编译为嵌入资源。每条文本的父元素包含一个键属性,该属性用作检索本地化文本的查找。下面是英文定义文件LangEN.xml的内容示例
<LangSettings>
<IsRtl>0</IsRtl>
<MinFontSize>11</MinFontSize>
<HeadingFontSize>16</HeadingFontSize>
<UIText>
<!-- Menu bar -->
<Entry key="TransformLabel">Transform</Entry>
<Entry key="LanguageLabel">Language</Entry>
<!-- Common Operations -->
<Entry key="ApplyLabel">Apply</Entry>
<Entry key="UndoLabel">Undo</Entry>
<Entry key="CancelLabel">Cancel</Entry>
<!-- Section Headings -->
<Entry key="InputLabel">Input</Entry>
<Entry key="OutputLabel">Output</Entry>
<Entry key="InfoLabel">Info</Entry>
<Entry key="TransformPropertiesLabel">Transform</Entry>
<!-- Item Labels -->
<Entry key="FlipRotateLabel">Flip / Rotate</Entry>
<Entry key="OffsetLabel">Offset</Entry>
<Entry key="ScaleToLabel">Scale To</Entry>
<Entry key="DimensionsLabel">Dimensions</Entry>
<Entry key="WidthLabel">Width</Entry>
<Entry key="HeightLabel">Height</Entry>
<Entry key="GoLabel">Go</Entry>
</UIText>
</LangSettings>
在上面的英文示例中,还请注意 IsRtl
、MinFontSize
和 HeadingFontSize
元素。字体大小用于确定渲染字体的尺寸,以提高可读性,尤其是在显示日语、韩语和阿拉伯语等文本时。IsRtl
元素表示语言是否从右到左阅读(如阿拉伯语和希伯来语)。
请注意,语言名称未在此 XML 中给出。这是因为本地化语言名称定义在单独的 XML 文件LanguageNames.xml中
<LangNames>
<Language code="en">English</Language>
<Language code="ar">العربية</Language>
<Language code="de">Deutsch</Language>
<Language code="el">Ελληνικά</Language>
<Language code="es">Español</Language>
<Language code="fr">Français</Language>
<Language code="he">עברית</Language>
<Language code="hi">हिन्दी</Language>
<Language code="it">Italiano</Language>
<Language code="jp">日本語</Language>
<Language code="ko">한국어</Language>
<Language code="ru">Русский</Language>
<Language code="sv">Svenska</Language>
</LangNames>
请注意,每个语言定义文件的名称都遵循一个约定:'LangXX.xml',其中 XX 对应于在 LanguageNames.xml 中为每个 Language
元素使用的两字母 ISO 语言代码。此约定可以扩展或改编以处理区域设置(例如,en-NZ、en-US)甚至三字母 ISO 语言代码。
UILanguageDefn 类
活动界面语言的语言定义文件中的数据被加载到一个内部类 UILanguageDefn
中,供应用程序其余部分使用。其主要组件是一个 Dictionary<string, string>
,其中包含从文本键到本地化文本值的映射。其他属性公开 IsRtl
、MinFontSize
和 HeadingFontSize
值。
使用此类时,通过调用以下方法检索本地化语言文本
/// <summary>
/// Gets the localised text value for the given key.
/// </summary>
/// <param name="key">The key of the localised text to retrieve.</param>
/// <returns>The localised text if found, otherwise an empty string.</returns>
public string GetTextValue(string key)
{
if (_uiText.ContainsKey(key))
return _uiText[key];
return "";
}
除此之外,UILanguageDefn
类还包含一个静态映射,将语言代码映射到从 LanguageNames.xml 加载的本地化语言名称,例如“en”到“English”和“sv”到“Svenska”。这用于填充“语言”选项卡中可用语言的列表,并由另一个列表进行过滤——即应用程序支持的权威语言列表。因此,即使存在语言定义文件和 LanguageNames.xml 中的条目,列表中不存在的任何语言都不会显示在 UI 中。这将在下一节中进一步说明。
加载数据
UILanguageDefn
类仅构成模型的一部分。模型中的第二个主要实体是应用程序范围的状态 MainWindowModel
。它包含整个应用程序正在使用的 UILanguageDefn
的权威实例。正是这个实例通过视图模型(ViewModel)绑定到整个 UI 中的文本元素。
当 MainWindowModel
被构造时,它首先注册权威语言列表,并从 LanguageNames.xml 资源文件加载本地化语言名称,然后加载当前语言。以下是其工作原理的简化视图
public class MainWindowModel
{
private UILanguageDefn _languageMapping;
public MainWindowModel(int maxWidth, int maxHeight)
{
RegisterLanguages();
LoadLanguageList();
//Settings are loaded here, where CurrentLanguageCode is decided.
UpdateLanguageData();
}
public string CurrentLanguageCode
{
get
{
// Retrieves the current language code from
// the Settings model (abstracted away)
}
}
/// <summary>
/// Registers the languages by their corresponding ISO code.
/// </summary>
private void RegisterLanguages()
{
// Defined in Constants class
string[] supportedLanguageCodes =
{
"en", "ar", "de", "el",
"es", "fr", "ko", "hi",
"it", "he", "jp", "ru", "sv"
};
foreach(string languageCode in supportedLanguageCodes)
UILanguageDefn.RegisterSupportedLanguage(languageCode);
}
/// <summary>
/// Loads the list of available languages from the embedded XML resource.
/// </summary>
private void LoadLanguageList()
{
// Defined in Constants class
string resourcePath = "RePaverModel.LanguageData.LanguageNames.xml";
System.IO.Stream file =
Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath);
XmlDocument languageNames = new XmlDocument();
languageNames.Load(file);
UILanguageDefn.LoadLanguageNames(languageNames.DocumentElement);
}
/// <summary>
/// Updates the UI language data from that
/// defined in the corresponding language file.
/// </summary>
/// <returns>
public bool UpdateLanguageData()
{
string languageCode = CurrentLanguageCode;
if (String.IsNullOrEmpty(languageCode)) return false;
//This follows a convention for language definition files
//to be named 'LangXX.xml' (or 'LangXX-XX.xml')
//where XX is the ISO language code.
string resourcePath =
String.Format(Constants.LanguageDefnPathTemplate, languageCode.ToUpper());
System.IO.Stream file =
Assembly.GetExecutingAssembly().GetManifestResourceStream(resourcePath);
XmlDocument languageData = new XmlDocument();
languageData.Load(file);
_languageMapping = new UILanguageDefn();
_languageMapping.LoadLanguageData(languageData.DocumentElement);
return true;
}
}
您可能会注意到上面的代码提到了第三个主要实体——设置状态。正是此状态存储了当前正在使用的界面语言以及运行时可能设置的其他设置。几乎所有设置都会在应用程序关闭时持久化到磁盘,并在下次打开应用程序时重新加载。
但是,如果应用程序是首次加载(并且没有设置文件),则必须将这些设置重置为默认值。对于语言,将英语设为默认值是最简单的,但这不用户友好。相反,通过检索查询当前系统语言
CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
并查找相应的语言。如果应用程序不支持该语言,则默认使用英语。这样,只要您的母语受支持,当您首次运行应用程序时,UI 将以您的母语显示。这在代码中的样子,位于设置模型层次结构中
public LanguageSettings()
{
// Initialise the default language code.
// In most cases this will be overwritten by the
// restored value from the saved settings, or that of the current culture.
_uiLanguageCode = Constants.DefaultLanguageCode; //"en"
string languageCode = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
// If the system language is supported, this will
// ensure that the application first loads
// with the UI displayed in that language.
if (UILanguageDefn.AllSupportedLanguageCodes.Contains(languageCode))
_uiLanguageCode = languageCode;
}
此类的另一个方法,稍后可能会调用(如果存在已保存的用户设置),它会用从已保存的设置文件中提取的值覆盖 _uiLanguageCode
。
视图模型
MVVM 实现与 WPF 和 Silverlight 应用程序中的模型-视图-呈现器(MVP)在此处有所不同。在 MVP 中,我们会有一个 Presenter,它将当前语言定义(或本地化文本的各个部分)传递给 View,View 将负责在 UI 中显示/更新文本。鉴于我们使用的是 WPF,文本可以通过数据绑定最轻松地更新;鉴于语言定义将在整个应用程序中使用(即在组件控件或窗口中),我们需要一个共享类来存储“当前语言”属性,以便在数据绑定发生时 UI 的任何部分都可以检索它。
在 MVVM 中,这样的共享类将与(例如 MainWindowViewModel
)其他视图模型一起构成视图模型层。该类 CommonViewModel
被实现为单例,以便静态实例属性(Current
)可以被分配为 Binding
的 Source
属性。因此,非静态属性通过绑定的 Path
属性进行引用。另一个关键细节是,视图模型实现了 INotifyPropertyChanged
接口,以便在源值更改时 UI 可以自动更新绑定。
以下是 UI 绑定的 CommonViewModel
属性,给定 UILanguageDefn
类中定义的数据
/// <summary>
/// Gets or sets the language definition used by the entire interface.
/// </summary>
/// <value>The language definition.</value>
public UILanguageDefn LanguageDefn
{
get { return _languageDefn; }
set
{
if (_languageDefn != value)
{
_languageDefn = value;
OnPropertyChanged("LanguageDefn");
OnPropertyChanged("HeadingFontSize");
OnPropertyChanged("MinFontSize");
OnPropertyChanged("IsRightToLeft");
}
}
}
public double HeadingFontSize
{
get
{
if (_languageDefn != null)
return (double)_languageDefn.HeadingFontSize;
return (double)UILanguageDefn.DefaultHeadingFontSize;
}
}
public double MinFontSize
{
get
{
if (_languageDefn != null)
return (double)_languageDefn.MinFontSize;
return (double)UILanguageDefn.DefaultMinFontSize;
}
}
public bool IsRightToLeft
{
get
{
if (_languageDefn != null)
return _languageDefn.IsRightToLeft;
return false;
}
}
MainWindowViewModel
是视图模型层次结构中最顶层的,它负责在 MainWindowModel
中的值发生更改时更新 CommonViewModel
中的当前语言。
/// <summary>
/// Refreshes the UI text to display in the current language.
/// </summary>
public void RefreshUILanguage()
{
_model.UpdateLanguageData();
CommonViewModel.Current.LanguageDefn = _model.CurrentLanguage;
//Notify any other internal logic to prompt a refresh (as necessary)
if (LanguageChanged != null)
LanguageChanged(this, new EventArgs());
}
视图
正如我一直在提到的,本地化文本通过数据绑定在视图中显示。但是,WPF 本身不知道如何处理 UILanguageDefn
,更不用说如何提取正确的本地化文本值了。这就是解谜的最后一部分。
值转换器
请记住,CommonViewModel.Current.LanguageDefn
是一个 UILanguageDefn
,而不是 TextBlock
的 Text
属性所期望的 string
,因此需要一个值转换器来执行此转换。此转换器使用 ConverterParameter
来指定查找键,以从 UILanguageDefn
实例中检索相应的本地化文本片段。请记住,在界面更改时更改的是 UILanguageDefn
。
这样做的妙处在于,对于界面中要本地化的每一段新文本,都需要将相应的元素添加到语言 XML 文件中,确保 ConverterParameter
和元素名称匹配。无需在 ViewModel、UILanguageDefn
或模型的其他部分之间定义其他属性。
转换器本身相对直接。它所需要做的就是实现 IValueConverter
(在 System.Windows.Data
中),并在类级别指定 ValueConversion
属性
[ValueConversion(typeof(UILanguageDefn), typeof(string))]
并按如下方式实现 Convert
函数
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
string key = parameter as string;
UILanguageDefn defn = value as UILanguageDefn;
if (defn == null || key == null) return "";
return defn.GetTextValue(key);
}
绑定
现在我们有了值转换器,就可以将其全部放入 Binding
表达式中
<TextBlock Text="{Binding Path=LanguageDefn,
Converter={StaticResource UIText}, ConverterParameter=ApplyLabel,
Source={x:Static vm:CommonViewModel.Current}}" />
为了使此工作正常进行,必须为 vm
(指向视图模型的命名空间)定义 XML 命名空间,并定义 UIText
资源(假设 conv
是值转换器的 XML 命名空间)
zlt;conv:UITextLookupConverter x:Key="UIText" />
简洁明了 - 自定义标记扩展
如果您像我一样继续使用它,您会发现重复相同的、冗长的 Binding 表达式在您的大多数 XAML 文件中会变得乏味。更不用说在重构过程中重命名类或属性了!
当然,有一种方法可以使其更加简洁,鉴于这些绑定之间唯一的变化是 ConverterParameter
。我选择的解决方案是使用自定义标记扩展。
为此,自定义标记扩展只是一个派生自 MarkupExtension
(在 System.Windows.Markup
中)的类,并根据约定命名为 [name]Extension
。其核心是需要完成的主要工作是重写 ProvideValue
方法。但这应该做什么?
这个自定义标记扩展的全部意义在于能够像这样在 XAML 中编写
<TextBlock Text="{ext:LocalisedText Key=ApplyLabel}" />
因此,自定义扩展称为 LocalisedTextExtension
,并添加了一个名为 Key
的公共 string
属性。因为后台仍然使用 Binding
,所以我创建了一个私有的 Binding
字段并在构造函数中实例化它,如下所示
public LocalisedTextExtension()
{
_lookupBinding = UITextLookupConverter.CreateBinding("");
}
其中静态 CreateBinding
方法在值转换器中定义为
public static Binding CreateBinding(string key)
{
Binding languageBinding = new Binding("LanguageDefn")
{
Source = CommonViewModel.Current,
Converter = _sharedConverter,
ConverterParameter = key,
};
return languageBinding;
}
因此,在定义了 Binding
之后,Key
属性只需获取和设置 ConverterParameter
。这样就剩下 ProvideValue
方法来施展其魔力
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _lookupBinding.ProvideValue(serviceProvider);
}
请记住,Binding
*是*一个 MarkupExtension
,因此它有自己的 ProvideValue
方法,我们可以利用它。
反复进行 - 字体大小和流向
对于某些语言,字符集包含复杂的字形,以至于在对于我们基于拉丁字母的字母表来说可以接受的字号下很难阅读。您会注意到 CommonViewModel
公开了 HeadingFontSize
和 MinFontSize
属性。这些属性分别提供本地化标题和所有剩余本地化文本的字体大小,例如,日语的字体大小将大于英语。
幸运的是,可以通过共享样式(在应用程序范围的资源字典中定义)以相似的模式绑定到这些样式,而无需值转换器
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="{Binding Path=MinFontSize,
Source={x:Static vm:CommonViewModel.Current}}" />
<!-- Remaining setters ... -->
</Style>
这是一张并排图,显示了差异
还有一些语言是从右到左阅读的,例如阿拉伯语和希伯来语。为了正确地将 UI 本地化到这些语言,反转界面是有意义的,否则,如果逻辑顺序与阅读顺序不一致,尝试使用该应用程序将有些令人困惑。
幸运的是,WPF 有一个方便的(附加)属性可以完成反转整个 UI 的繁重工作:FrameworkElement.FlowDirection
。
这样做非常强大,因为我只需要在包含在主窗口中的根级控件上进行一次绑定,因为这个值会被其下方的每个 FrameworkElement
在视觉层次结构中继承。此绑定只需要查看 CommonViewModel
的 IsRightToLeft
属性,并通过(另一个值转换器)将其转换为 FlowDirection
枚举。创建了一个自定义标记扩展,遵循与之前类似的模式,以简化 XAML 到此
<Window x:Class="RePaver.UI.MainWindow" ... >
<DockPanel FlowDirection="{ext:LocalisedFlowDirection}">
<!-- Contents -->
</DockPanel>
</Window>
鉴于其强大功能,以下是一些需要考虑的点/陷阱
- 自定义面板会自动反向布局,因此您不需要创建
IsReversed
属性(或类似属性)并相应地调整ArrangeOverride
计算。 - 位图图像和形状(例如路径)会被反转。如果您想保留这些(例如用于公司徽标/品牌)的渲染,独立于流向,那么您需要通过将其设置为
LeftToRight
来覆盖FlowDirection
。 - 如果界面具有
RightToLeft
的FlowDirection
并且一个元素(例如 Image)具有LeftToRight
的FlowDirection
,那么该元素的Margin
将以RightToLeft
的方式起作用。由于Padding
作用于元素的内部视觉层次结构,因此填充将以LeftToRight
的方式起作用。 - 包含语言无关数据的 TextBox 应将
FlowDirection
设置为LeftToRight
。理想情况下,这应该在样式中设置,以最大程度地减少重复并确保一致性。
所以这是必然的“之后”截图
请注意,路径、旋转选择控件以及输入/输出文本框都以从左到右的方式呈现,而不管语言如何。这是因为这些元素特定于问题域,如果它们以从右到左的方式显示,那将没有意义(并且会造成一些混淆)。
结论
就这样——一个可以动态更改 UI 语言的本地化 WPF 应用程序。首次在法国的本地计算机上运行它,瞧,它以法语显示。这一切都来自同一个语言版本。
最后一点要注意但未详细介绍的是,整个 UI 以流式布局,以便自动调整布局以适应内容。不是显式设置控件的宽度和高度、网格行/列定义等,而是将它们保留为“自动”,同时可以定义最小和最大尺寸。这是更通用的最佳实践之一(而不是特定于本地化),但不遵循此实践在切换语言时会显现出来。
后记
软件开发界本地化是一个热门话题,这并不奇怪,所以自然我不是唯一一个写这个的人。事实证明,此后我发现还有其他人做过类似的事情
- Sebastian Przybylski(文章)也将 UI 文本存储在 XML 文件中作为嵌入资源,但 XAML 直接绑定到 XML 资源而不是视图模型。
- David Sleeckx(文章)使用自定义标记扩展来从本地缓存检索翻译文本,或者调用 Google 语言 API 进行实时翻译。
- “SeriousM”已将WPF Localization Extension 上传到 CodePlex。此实现使用自定义标记扩展从资源文件/卫星资源程序集中提取本地化文本(和其他值)。
显然,现在有几种本地化 WPF 应用程序的选项——它们并非相互排斥。根据您的优先级,我实现的某些部分可能适用于您应用程序的特定区域,而替代实现的某些部分则适用于其他区域。可以根据需要进行定制。
历史
- 2009 年 8 月 5 日:初始版本。