構建多語言的 WPF 應用
導言
在WPF應用程序中搭建多語言支持(Multilingual Support)是我最近在做的一件事,對于不使用英語的人士而言,此舉提高了程序的可用性。實現起來要完成以下目標:
-
一個版本容納多種語言. 這就意味著不要創(chuàng)建單獨的英語版本、法語版本、日語版本等等。 許多電子產品(例如電視和數碼相機)在同一模塊中支持多語言。你不需要購買不同模塊或給軟件打補丁來得到與默認設置不同的語言
-
允許在運行時切換接口語言. 這就是說不需要關閉應用程序并配置操作系統(tǒng)環(huán)境,一切都交給安裝器。
-
***運行選擇合適語言. W應用程序***次運行,就把接口語言設為操作系統(tǒng)的系統(tǒng)語言。這點很有意義--法國用戶喜歡安裝、運行、并馬上使用軟件,而不會再一個不熟悉的應用程序中找到切換語言的地方。
-
允許UI可拓展以便翻譯,縮減可能的裁剪文本
此外,具體實現不應該隨著用戶界面的增長而越來越來難實現。(這是我覺得最困難的方面。)
所以這篇文章旨在提供一份我開發(fā)過程的詳細解決方案的大綱,這基于一些我過去寫過的博客和帖子(這里,這里和這里)。隨著時間的推移,我將指出例子的相關部分并告訴你它們是如何適配在一起的。
聲明:例子中的文本是使用自動在線翻譯服務生成的。盡管盡了***的努力來確保這是盡可能準確(通過反向翻譯校對),有可能翻譯的內容有不準確或錯誤。特別是當它使用了一個我不清楚的完全不同的寫作系統(tǒng)。
上層概述
這個為WPF應用所設計的實現遵循了一種MVVM(模型-視圖-視圖模型)樣式。語言數據存儲在嵌入式XML文件中,這些文件按照需求原則加載到內存中,即當接口語言發(fā)生改變的時候。這就是“模型”的部分。.
“視圖模型”具有將當前語言的語言數據包含到整個WPF應用中的特性。它是XAML文件的集合,XAML文件形成了包含了關聯該語言數據的“視圖”。為了給一個特定的文本元素選擇準確的值,每個關聯都利用具有一個轉換器參數的用戶定制值轉化器來查找文本鍵值。***,用一個用戶定制標記擴展來抽取這個關聯的細節(jié),這樣只有鍵值(即轉換參數)需要指定。
例子
為了說明這個實現在實際中如何工作,我根據這個功能創(chuàng)建一個小的示例應用。這個叫做'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)中是為了被剩下的應用消耗掉。主要的組件是一個<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類有一個靜態(tài)的從語言編碼到局部的語言名稱的映射(這個映射是從LanguageNames.xml中加載進來的),例如,“en”和“English”、“sv”和“Svenska”。這被用來填充到'Language'標簽的可用語言列表中,而且被應用所支持的權威的語言列表過濾。因此,任何不再這個列表的語言不會被界面所顯示。即使有一個語言定義文件或在LanguageNames.xml中有所對應的實體,也不會顯示這個語言。這會在下面的章節(jié)中進一步介紹。
#p#
加載數據
類`UILanguageDefn`形成模型的一部分。模型里面的第二個主要的實體就是應用全狀態(tài),`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;
- }
- }
你可能注意到上面的代碼提到了第三個主體 - 設置狀態(tài)。在眾多可在運行時調整的設置中,正是這個狀態(tài)存儲了當前正被使用的接口語言。大多數的設置項都在應用程序關閉后保存在磁盤中,當程序再次打開時就重新加載出來。
然而,如果應用程序是***次打開(沒有設置文件存在),那么這些設置就會被設定為默認狀態(tài)。對于語言來說,英語是默認的,但這并不是用戶友好(user-friendly)的。所以呢,我們就這樣檢索當前系統(tǒng)語言:
- 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)來實現,這樣靜態(tài)實例屬性Current就可以作為一個綁定的源屬性來賦值了。非靜態(tài)屬性則通過綁定的Path屬性來引用。還有一點很重要,ViewModel實現了INotifyPropertyChanged的接口,以致UI能在源數值發(fā)生改變時自動更新綁定。
這里是綁定到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處在ViewModel架構最前端, 負責在MainWindowModel值發(fā)生變化時,更新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());
- }
#p#
視圖
正如我所提到的,本地化文本通過數據綁定顯示到視圖中。然而WPF自身并不知道如何處理UILanguageDefn類,更不用說提取合適的本地化文本值。這也是***一個難題。
值轉換器
請記住,CommonViewModel.Current.LanguageDefn是一個UILaunguageDefn,不是TextBlock的Text屬性期待的一個字符串。因此,此時需要一個值轉換器來完成這項轉換工作。這個值轉換器使用ConverterParameter來指定創(chuàng)建查找關鍵字,用來恢復來自UILanguage實例中局部符合條件的文本。記住,當接口改變了,UILanuageDefn也改變。
這項工作的優(yōu)點在于對每一段局限在接口當中的文本,符合條件的元素需要被添加到language XML文件,確保ConverterParameter和元素名稱匹配。此外不需要定義任何額外的屬性——不管是在視圖層,UILanguageDefn,還是在模型層的其他部分。
這個converter相對簡單. 只需在類級別上指定 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);
- }
綁定
現在我們獲得了了一個 value converter, 我們可以將它放置在一個 Binding 表達式中:
- <TextBlock Text="{Binding Path=LanguageDefn,
- Converter={StaticResource UIText}, ConverterParameter=ApplyLabel,
- Source={x:Static vm:CommonViewModel.Current}}" />
如果想要它工作, 這個 XML 的 命名空間必須設置為 vm(指向 ViewModel的命名空間),并且 UIText 的資源需要被定義 (假設conv 是這個 value converter 的 XML 的命名空間):
- zlt;conv:UITextLookupConverter x:Key="UIText" />
簡單明了——自定義標記擴展
如果你當前的狀態(tài)(像我一樣)又想要愉快的方式,在大多數的XAML文件中的長綁定表達式里,你發(fā)現它變得乏味,是同一樣東西的重復。甚至不考慮重命名類或者把屬性作為重構的一部分!
當然,有一種方式能使其更簡潔,考慮到這些綁定之間的唯一變化就是ConverterParameter。解決方案是使用使用自定義標記擴展。
為了做到這一點,自定義標記擴展是一個簡單的類,它派生自MarkupExtension(在System.Windows.Markup),按照慣例被命名為[name]Extension。在其核心處,關鍵點是需要重載ProvideValue方法。但是這該怎么做呢?
自定義標記拓展的重點就是在XAML中寫下類似這樣的代碼:
- <TextBlock Text="{ext:LocalisedText Key=ApplyLabel}" />
因此,自定義拓展被稱作LocalisedTextExtension,并添加一個Key,它的類型是public string.因為在后臺中,綁定一直處于使用狀態(tài),所以我創(chuàng)建了一個private 綁定域,并從構造器中實例化它 :
- public LocalisedTextExtension()
- {
- _lookupBinding = UITextLookupConverter.CreateBinding("");
- }
而靜態(tài)的CreateBinding方法定義在值轉換器(value converter)中:
- public static Binding CreateBinding(string key)
- {
- Binding languageBinding = new Binding("LanguageDefn")
- {
- Source = CommonViewModel.Current,
- Converter = _sharedConverter,
- ConverterParameter = key,
- };
- return languageBinding;
- }
所以定義好了Binding后,可以通過ConverterParameter參數來獲取和設置Key屬性的值。這也使得ProvideValue方法可以大展身手:
- public override object ProvideValue(IServiceProvider serviceProvider)
- {
- return _lookupBinding.ProvideValue(serviceProvider);
- }
而一個Binding是一個MarkupExtension,所以它有自己的可以調用的ProvideValue方法。
#p#
Rinse and Repeat - 字體大小與流方向
某些語言的字符集包含十分復雜的圖形元素,以致在拉丁文可以辨認的字符大小,用來顯示這些語言的時候,變得模糊不清了。你注意到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的枚舉值。自定義的標記擴展被創(chuàng)建,遵循以前類似的模板,簡化為XAML:
- <Window x:Class="RePaver.UI.MainWindow" ... >
- <DockPanel FlowDirection="{ext:LocalisedFlowDirection}">
- <!-- Contents -->
- </DockPanel>
- </Window>
鑒于到上述功能的強大,這里仍然要考慮一些陷阱和要點:
-
自定義面板自動反轉布局,所以你不需要創(chuàng)建一個IsRevered屬性(或者類似的)或者按照你的估算調整ArrangeOverride。
-
位圖和形狀(如線路)是反轉的。如果您想要保留這些,呈現獨立的流向(如公司的logo或者商標),那么你需要重寫FlowDirection,設置它為LeftToRight。
-
如果接口有RightToLeft的FlowDirection,而元素(如Image)具有LeftToRight的FlowDirection,那么元素的Margin會以RightToLeft的方式展示。由于Padding展示在元素內部可視層次,所以一個padding將會以LeftToRight的方式展示。
-
TextBoxes包含語言恒定的數據,應當將FlowDirection設置為LeftToRight。理想情況下,此屬性應設置為盡量減少重復并保證一致性的風格。
所以,下面就是趕時髦的“處理后”的截圖:
注意路徑,旋轉選擇控件,輸入輸出文本框是以從左至右的方式展示,這與語言無關。這是因為這些元素是特定的問題區(qū)域,如果它們以從右至左的方式展示,就沒有道理了(可能會引起誤解)。
總結
現在明白了——一個局部的WPF應用程序可以在運行時動態(tài)地改變UI。***次運行它是在法語的本地計算機環(huán)境中,瞧, il est affiché en Français. 它們都來自同一種語言版本。
***一個要點需要注意,這里不做詳細介紹,整個UI布局以流體方式布局,這樣的布局會自動調整以適應內容。 而不是顯式地設置寬度和高度, 網格的行/列定義,等等。這些都是“自動”為左的,同時還可以定義最小和***值。這是很普通的實例中***的一個(而不是特定的本地化), 但當切換語言的時候,不允許這樣的實例真的顯示出來。
后記
軟件開發(fā)中本地化是一個熱門的話題,理所當然,我也不是唯一一個寫這方面的人。事實上,我也發(fā)現了一些人在做同樣的事:
-
Sebastian Przybylski (article) 也把UI文本存儲在XML文件作為嵌入資源,而把XAML直接綁定到XML資源上而不是通過ViewModel.
-
David Sleeckx (article) 使用自定義標記拓展來檢索本地緩存的翻譯文本,或者調用Google語言API來實現實時翻譯。
-
'SeriousM' 在CodePlex上更新了 WPF本地化拓展 . 它是通過提取資源文件/資源程序集中的本地化文本(或其他值)來實現的。
顯然,實現WPF程序的本地化有很多種選擇,它們并不互斥。根據你的權衡,我所提到的實現方法僅適用于你程序的部分,另一部分則會出現在其他的地方。所以你要根據你的需求,隨意調整實現方法。
英文原文:Building Multilingual WPF Applications
譯文來自:http://www.oschina.net/translate/building-multilingual-wpf-applications