导言
在WPF应用程序中搭建多语言支持(Multilingual Support)是我最近在做的一件事,对于不使用英语的人士而言,此举提高了程序的可用性。实现起来要完成以下目标:
一个版本容纳多种语言. 这就意味着不要创建单独的英语版本、法语版本、日语版本等等。 许多电子产品(例如电视和数码相机)在同一模块中支持多语言。你不需要购买不同模块或给软件打补丁来得到与默认设置不同的语言 允许在运行时切换接口语言. 这就是说不需要关闭应用程序并配置操作系统环境,一切都交给安装器。 首次运行选择合适语言. W应用程序第一次运行,就把接口语言设为操作系统的系统语言。这点很有意义--法国用户喜欢安装、运行、并马上使用软件,而不会再一个不熟悉的应用程序中找到切换语言的地方。允许UI可拓展以便翻译,缩减可能的裁剪文本
此外,具体实现不应该随着用户界面的增长而越来越来难实现。(这是我觉得最困难的方面。)
所以这篇文章旨在提供一份我开发过程的详细解决方案的大纲,这基于一些我过去写过的博客和帖子(这里,这里和这里)。随着时间的推移,我将指出例子的相关部分并告诉你它们是如何适配在一起的。
声明:例子中的文本是使用自动在线翻译服务生成的。尽管尽了最大的努力来确保这是尽可能准确(通过反向翻译校对),有可能翻译的内容有不准确或错误。特别是当它使用了一个我不清楚的完全不同的写作系统。
上层概述
这个为WPF应用所设计的实现遵循了一种MVVM(模型-视图-视图模型)样式。语言数据存储在嵌入式XML文件中,这些文件按照需求原则加载到内存中,即当接口语言发生改变的时候。这就是“模型”的部分。.
“视图模型”具有将当前语言的语言数据包含到整个WPF应用中的特性。它是XAML文件的集合,XAML文件形成了包含了关联该语言数据的“视图”。为了给一个特定的文本元素选择准确的值,每个关联都利用具有一个转换器参数的用户定制值转化器来查找文本键值。最后,用一个用户定制标记扩展来抽取这个关联的细节,这样只有键值(即转换参数)需要指定。例子
为了说明这个实现在实际中如何工作,我根据这个功能创建一个小的示例应用。这个叫做'RePaver'的应用用于清除路径标记表达式,并具有基本的翻转,反转,转换和缩放实际几何图形(即无需图层转换)的功能。在后台,该应用用正则表达式抽取路径段落,并就地对每个段落进行转换。
为了给你有个概念,看如下一个Path表达式的例子,这个表达式一般从导出为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" ... />如果你复制黏贴(引号中的)数据表达式到输入框中并点击'Go',可以看到如下的输出:
M 4,16 L 4,6 L 0,6 L 6,0 L 6,16 Z在右边你还能即时看到形象化的"转换前"和"转换后"的结果。
你可以任意设置一些选项 - 可以看到这些操作是按照翻转/反转 -> 缩放到[根据边框尺寸] -> 偏移。当然,你可以用不同的语言试一下。
模块
XML
如上所述,每个组成用户界面的文本都保存在每种语言的XML文件的本地化表格中, 并把XML文件当做嵌入式资源来编译。每条text的父元素包含一个键属性用来检索本地化文本。下面是英语版本定义文件的例子,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>在上述英文版本示例中,同样提到了 theIsRtl, MinFontSize, 和HeadingFontSize元素。字体大小用来决定渲染字体的尺寸,让字体更易分辨,尤其在显示日文,韩文和阿拉伯文的时候。IsRtlel元素决定语言是否从右往左读(阿拉伯文和希伯来语就是这样)。
注意到语言名称并没有出现在上面的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 与两个字母的 ISO语言代码相对应,LanguageNames.xml中的每个Language元素也该代码对应。当然,这一惯例可以拓展或修改为易于处理本地化(如 en-NZ, en-US),甚至改成三字母的ISO语言代码。
UILanguageDefn类在语言定义文件中的当前界面语言数据被加载进一个内部类(UILanguageDefn)中是为了被剩下的应用消耗掉。主要的组件是一个类型的字典。这个字典包含了从文本键到局部的文本值的映射。其它的属性显示: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”。这被用来填充到'Language'标签的可用语言列表中,而且被应用所支持的权威的语言列表过滤。因此,任何不再这个列表的语言不会被界面所显示。即使有一个语言定义文件或在LanguageNames.xml中有所对应的实体,也不会显示这个语言。这会在下面的章节中进一步介绍。
加载数据
类UILanguageDefn形成模型的一部分。模型里面的第二个主要的实体就是应用全状态,MainWindowModel。它包含了被整个应用程序使用的UILanguageDefn的授权的实例。这是在全部界面中获取文本元素的边界的实例。(通过ViewModel)。
当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; } }你可能注意到上面的代码提到了第三个主体 - 设置状态。在众多可在运行时调整的设置中,正是这个状态存储了当前正被使用的接口语言。大多数的设置项都在应用程序关闭后保存在磁盘中,当程序再次打开时就重新加载出来。
然而,如果应用程序是第一次打开(没有设置文件存在),那么这些设置就会被设定为默认状态。对于语言来说,英语是默认的,但这并不是用户友好(user-friendly)的。所以呢,我们就这样检索当前系统语言:
CultureInfo.CurrentCulture.TwoLetterISOLanguageName;找到相应的语言后,如果应用程序不支持该语言,就让英语作为默认语言。这样,只要你的本地语言受支持,UI就会在你程序第一次运行时显示该语言。在Setting model hierarchy中,有如下代码
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应用程序中的Model-View-Presenter(MVP).在MVP模式中,我们需要一个Presenter把当前语言的定义(或单个的本地化后的文本)传给视图(View),由视图负责UI中文本的显示与更新。考虑到我们在使用WPF,文本的更新可以很容易地通过数据绑定来实现;考虑到语言定义要在整个应用程序(组件或窗体)中使用,我们需要一个共享类来保存当前语言属性,这样当进行数据绑定时,就能使该属性在UI的任何一部分检索出来。
在MVVM模式中,这个共享类同其他视图模型(例如MainWindowViewModel)一道,将成为组成视图模型层的一部分。CommonViewModel这个类是作为单例模式(Singleton)来实现,这样静态实例属性Current就可以作为一个绑定的源属性来赋值了。非静态属性则通过绑定的Path属性来引用。还有一点很重要,ViewModel实现了INotifyPropertyChanged的接口,以致UI能在源数值发生改变时自动更新绑定。
相关资源:七夕情人节表白HTML源码(两款)