基于 WPF 模块化架构下的本地化设计实践
背景描述
最近接到一個需求,就是要求我們的 WPF 客戶端具備本地化功能,實現中英文多語言界面。剛開始接到這個需求,其實我內心是拒絕的的,但是沒辦法,需求是永無止境的。所以只能想辦法解決這個問題。
首先有必要說一下我們的系統架構。我們的系統是基于?Prism?來進行設計的,所以每個業務模塊之間都是相互獨立,互不影響的?DLL,然后通過主?Shell?來進行目錄的動態掃描來實現動態加載。
為了保證在不影響系統現有功能穩定性的前提下,如何讓所有模塊支持多語言成為了一個亟待解決的問題。
剛開始,我 Google 了一下,查閱了一些資料,很多都是介紹如何在單體程序中實現多語言,但是在模塊化架構中,我個人覺得這樣做并不合適。做過本地化的朋友應該都知道,在進行本地化翻譯的時候,都需要創建對應語言的資源文件,無論是使用?.xaml?.resx?或?.xml,這里面會存放我們的本地化資源。對于單體系統而言,這些資源直接放到主程序下即可,方便快捷。但是對于模塊化架構的程序,這樣做就不太好,而是應該將這些資源都分別放到自己模塊內部由自己來維護,主程序只需規定整個系統的區域語言即可。
設計思路
面對上面的背景描述,我們可以大致描述一下我們期望的解決方式,主程序只負責對整個系統進行區域語言設置,每個模塊的本地化由本模塊內部完成,所有模塊的本地化切換方式保持一致,依賴于共有的一種實現。如下圖所示:
實現方案
由于如何使用?Prism?不是本文的重點,所以這里就略過主程序和模塊程序中相關的模板代碼,感興趣的小伙伴可以自行在園子里搜索相關技術文章。
參照上述的思路,我們可以做一個小示例來展示一下如何進行多模塊多語言的本地化實踐。
在這個示例中,我以 DotNetCore 3.0 版本的 WPF 和 Prism 進行示例說明。在我們的示例工程中創建三個項目
BlackApp
引用 Prism.Unity 包
WPF App(.NET Core 版本),作為啟動程序
BlackApp.ModuleA
引用 Prism.Wpf 包
WPF UseControl(.NET Core 版本),作為示例模塊
BlackApp.Common
ClassLibrary(.NET Core 版本),作為基礎的公共服務層
BlackApp.ModuleA 添加對 BlackApp.Common 的引用,并將 BlackApp 和 BlackApp.ModuleA 的項目輸出修改為相同的輸出目錄。然后修改對應的基礎代碼,以確保主程序能正常加載并顯示 ModuleA 模塊及其內容。
上述操作完成后,我們就可以編寫我們的測試代碼了。按照我們的設計思路,我需要先在 BlackApp.ModuleA 定義我們的本地化資源文件,對于這個資源文件的類型選擇,理論上我們是可以選擇任何一種基于 XML 的文件,但是不同類型的文件對于后面是否是埋坑行為這個需要認真考慮一下。這里我建議使用 XAML 格式的文件。我們在 BlackApp.ModuleA 項目的根目錄下創建一個?Strings?的文件夾,然后里面分別創建?en-US.xaml?和?zh-CN.xaml?文件。這里建議最好以語言名稱作為文件名稱,這樣方便到時候查找。文件內容如下所示:
en-US.xaml
zh-CN.xaml
資源文件定義好了,接下來就是如何使用了。
對于我們需要進行本地化的 XAML 頁面,首先我們需要指當前使用到的資源文件,這個時候就需要在我們的 BlackApp.Common 項目中定義一個依賴屬性了,然后通過依賴屬性的方式來進行設置。由于語言種類有很多,所以我們定義一個文件夾目錄的依賴屬性,來指定當前頁面需要用到的資源的文件夾路徑,然后由輔助類到時候依據具體的語言類型來到指定目錄查找指當的資源文件。
[RuntimeNameProperty(nameof(ExTranslationManager))] public class ExTranslationManager : DependencyObject {public static string GetResourceDictionary(DependencyObject obj){return (string)obj.GetValue(ResourceDictionaryProperty);}public static void SetResourceDictionary(DependencyObject obj, string value){obj.SetValue(ResourceDictionaryProperty, value);}public static readonly DependencyProperty ResourceDictionaryProperty =DependencyProperty.RegisterAttached("ResourceDictionary", typeof(string), typeof(ExTranslationManager), new PropertyMetadata(null));}本地化資源指定完畢后,我們就可以使用里面資源文件進行本地化操作。如果想在 XAML 對相應屬性進行?標簽式?訪問,需要定義一個繼承自?MarkupExtension?類的自定義類,并在該類中實現?ProvideValue?方法。接下來在我們的 BlackApp.Common 項目中定義該類,示例代碼如下所示:
[RuntimeNameProperty(nameof(ExTranslation))] public class ExTranslation : MarkupExtension {public string StringName { get; private set; }public ExTranslation(string stringName){this.StringName = stringName;}public override object ProvideValue(IServiceProvider serviceProvider){object targetObject = (serviceProvider as IProvideValueTarget)?.TargetObject;ResourceDictionary dictionary = GetResourceDictionary(targetObject);if (dictionary == null){object rootObject = (serviceProvider as IRootObjectProvider)?.RootObject;dictionary = GetResourceDictionary(rootObject);}if (dictionary == null){if (targetObject is FrameworkElement frameworkElement){dictionary = GetResourceDictionary(frameworkElement.TemplatedParent);}}return dictionary != null && StringName != null && dictionary.Contains(StringName) ?dictionary[StringName] : StringName;}private ResourceDictionary GetResourceDictionary(object target){if (target is DependencyObject dependencyObject){object localValue = dependencyObject.ReadLocalValue(ExTranslationManager.ResourceDictionaryProperty);if (localValue != DependencyProperty.UnsetValue){var local = localValue.ToString();var (baseName,stringName) = SplitName(local);var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml";var dict = new ResourceDictionary { Source = new Uri(str) };return dict;}}return null;}public static (string baseName, string stringName) SplitName(string name){int idx = name.LastIndexOf('.');return (name.Substring(0, idx), name.Substring(idx + 1));} }此外,如果我們的 ViewModel 中也有數據需要進行本地化操作的化,我們可以定義一個擴展方法,示例代碼如下所示:
public static class ExTranslationString {public static string GetTranslationString(this string key, string resourceDictionary){var (baseName, stringName) = ExTranslation.SplitName(resourceDictionary);var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml";var dictionary = new ResourceDictionary { Source = new Uri(str) };return dictionary != null && !string.IsNullOrWhiteSpace(key) && dictionary.Contains(key) ? (string)dictionary[key] : key;} }通過在 BlackApp.Common 中定義上述 3 個輔助類,基本可以滿足我們的需求,我們可以卻換到 BlackApp.ModuleA 項目中,并進行如下示例修改
View 層使用示例
ViewModel 層使用示例
最后,我們就可以在我們的 BlackApp 項目中的 App.cs 構造函數中來設置我們程序的語言類型,示例代碼如下所示:
public partial class App {public App(){CultureInfo ci = new CultureInfo("en-US");Thread.CurrentThread.CurrentCulture = ci;}protected override Window CreateShell(){return Container.Resolve<MainWindow>();}protected override void RegisterTypes(IContainerRegistry containerRegistry){}protected override IModuleCatalog CreateModuleCatalog(){return new DirectoryModuleCatalog() { ModulePath = AppDomain.CurrentDomain.BaseDirectory };} }寫到這里,我們應該就可以進行本地化的測試工作了,嘗試編譯運行我們的示例程序,如果不出意外的話,應該是可以通過在 主程序中設置區域類型來更改模塊程序中的對應本地化資源內容。
最后,整個示例項目的組織結構如下圖所示:
總結
對于模塊化架構的本地化實現,有很多的實現方式,我這里介紹的只是一種符合我們的業務場景的一種實現,期待大佬們在評論區留言提供更好的解決方案。
補充
經同事驗證,使用?.resx?格式的資源文件會更簡單一下,可以直接通過
的方式來訪問。但前提是需要將對應資源文件的訪問修飾符設置為?public。
參考
Localization of a WPF app - the simple approach
wpf-localization-multiple-resource-resx-one-language
LocalizeMarkupExtension
Markup Extensions and WPF XAML
總結
以上是生活随笔為你收集整理的基于 WPF 模块化架构下的本地化设计实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 通过Blazor使用C#开发SPA单页面
- 下一篇: 「数据ETL」从数据民工到数据白领蜕变之