背景描述
最近接到一個需求,就是要求我們的 WPF
客戶端具備本地化功能,實現(xiàn)中英文多語言界面。剛開始接到這個需求,其實我內(nèi)心是拒絕的的,但是沒辦法,需求是永無止境的。所以只能想辦法解決這個問題。
首先有必要說一下我們的系統(tǒng)架構(gòu)。我們的系統(tǒng)是基于 Prism 來進行設(shè)計的,所以每個業(yè)務(wù)模塊之間都是相互獨立,互不影響的 DLL,然后通過主 Shell
來進行目錄的動態(tài)掃描來實現(xiàn)動態(tài)加載。
為了保證在不影響系統(tǒng)現(xiàn)有功能穩(wěn)定性的前提下,如何讓所有模塊支持多語言成為了一個亟待解決的問題。
剛開始,我 Google
了一下,查閱了一些資料,很多都是介紹如何在單體程序中實現(xiàn)多語言,但是在模塊化架構(gòu)中,我個人覺得這樣做并不合適。做過本地化的朋友應(yīng)該都知道,在進行本地化翻譯的時候,都需要創(chuàng)建對應(yīng)語言的資源文件,無論是使用
.xaml .resx 或 .xml
,這里面會存放我們的本地化資源。對于單體系統(tǒng)而言,這些資源直接放到主程序下即可,方便快捷。但是對于模塊化架構(gòu)的程序,這樣做就不太好,而是應(yīng)該將這些資源都分別放到自己模塊內(nèi)部由自己來維護,主程序只需規(guī)定整個系統(tǒng)的區(qū)域語言即可。
設(shè)計思路
面對上面的背景描述,我們可以大致描述一下我們期望的解決方式,主程序只負責對整個系統(tǒng)進行區(qū)域語言設(shè)置,每個模塊的本地化由本模塊內(nèi)部完成,所有模塊的本地化切換方式保持一致,依賴于共有的一種實現(xiàn)。如下圖所示:
實現(xiàn)方案
由于如何使用 Prism 不是本文的重點,所以這里就略過主程序和模塊程序中相關(guān)的模板代碼,感興趣的小伙伴可以自行在園子里搜索相關(guān)技術(shù)文章。
參照上述的思路,我們可以做一個小示例來展示一下如何進行多模塊多語言的本地化實踐。
在這個示例中,我以 DotNetCore 3.0 版本的 WPF 和 Prism 進行示例說明。在我們的示例工程中創(chuàng)建三個項目
* BlackApp
* 引用 Prism.Unity 包
* WPF App(.NET Core 版本),作為啟動程序
* BlackApp.ModuleA
* 引用 Prism.Wpf 包
* WPF UseControl(.NET Core 版本),作為示例模塊
* BlackApp.Common
* ClassLibrary(.NET Core 版本),作為基礎(chǔ)的公共服務(wù)層
BlackApp.ModuleA 添加對 BlackApp.Common 的引用,并將 BlackApp 和 BlackApp.ModuleA
的項目輸出修改為相同的輸出目錄。然后修改對應(yīng)的基礎(chǔ)代碼,以確保主程序能正常加載并顯示 ModuleA 模塊及其內(nèi)容。
上述操作完成后,我們就可以編寫我們的測試代碼了。按照我們的設(shè)計思路,我需要先在 BlackApp.ModuleA
定義我們的本地化資源文件,對于這個資源文件的類型選擇,理論上我們是可以選擇任何一種基于 XML
的文件,但是不同類型的文件對于后面是否是埋坑行為這個需要認真考慮一下。這里我建議使用 XAML 格式的文件。我們在 BlackApp.ModuleA
項目的根目錄下創(chuàng)建一個Strings 的文件夾,然后里面分別創(chuàng)建 en-US.xaml 和 zh-CN.xaml
文件。這里建議最好以語言名稱作為文件名稱,這樣方便到時候查找。文件內(nèi)容如下所示:
* en-US.xaml <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BlackApp.ModuleA.Strings"
xmlns:system="clr-namespace:System;assembly=System.Runtime"> <system:String
x:Key="string1">Hello world</system:String> </ResourceDictionary>
* zh-CN.xaml <ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BlackApp.ModuleA.Strings"
xmlns:system="clr-namespace:System;assembly=System.Runtime"> <system:String
x:Key="string1">世界你好</system:String> </ResourceDictionary>
資源文件定義好了,接下來就是如何使用了。
對于我們需要進行本地化的 XAML 頁面,首先我們需要指當前使用到的資源文件,這個時候就需要在我們的 BlackApp.Common
項目中定義一個依賴屬性了,然后通過依賴屬性的方式來進行設(shè)置。由于語言種類有很多,所以我們定義一個文件夾目錄的依賴屬性,來指定當前頁面需要用到的資源的文件夾路徑,然后由輔助類到時候依據(jù)具體的語言類型來到指定目錄查找指當?shù)馁Y源文件。
示例代碼如下所示:
[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); } // Using a
DependencyProperty as the backing store for ResourceDictionary. This enables
animation, styling, binding, etc... public static readonly DependencyProperty
ResourceDictionaryProperty =
DependencyProperty.RegisterAttached("ResourceDictionary", typeof(string),
typeof(ExTranslationManager), new PropertyMetadata(null)); }
本地化資源指定完畢后,我們就可以使用里面資源文件進行本地化操作。如果想在 XAML 對相應(yīng)屬性進行 標簽式 訪問,需要定義一個繼承自
MarkupExtension 類的自定義類,并在該類中實現(xiàn) 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 中也有數(shù)據(jù)需要進行本地化操作的化,我們可以定義一個擴展方法,示例代碼如下所示:
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 層使用示例 <UserControl x:Class="BlackApp.ModuleA.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ex="clr-namespace:BlackApp.Common;assembly=BlackApp.Common"
xmlns:local="clr-namespace:BlackApp.ModuleA.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/" d:DesignHeight="300" d:DesignWidth="300"
ex:ExTranslationManager.ResourceDictionary="BlackApp.ModuleA.Strings"
prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d"> <Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock
Text="{Binding Message}" /> <TextBlock Text="{ex:ExTranslation string1}" />
</StackPanel> </Grid> </UserControl>
* ViewModel 層使用示例 "message".GetTranslationString("BlackApp.ModuleA.Strings")
最后,我們就可以在我們的 BlackApp 項目中的 App.cs 構(gòu)造函數(shù)中來設(shè)置我們程序的語言類型,示例代碼如下所示:
public partial class App { public App() { //CultureInfo ci = new
CultureInfo("zh-cn"); 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 }; } }
寫到這里,我們應(yīng)該就可以進行本地化的測試工作了,嘗試編譯運行我們的示例程序,如果不出意外的話,應(yīng)該是可以通過在
主程序中設(shè)置區(qū)域類型來更改模塊程序中的對應(yīng)本地化資源內(nèi)容。
最后,整個示例項目的組織結(jié)構(gòu)如下圖所示:
總結(jié)
對于模塊化架構(gòu)的本地化實現(xiàn),有很多的實現(xiàn)方式,我這里介紹的只是一種符合我們的業(yè)務(wù)場景的一種實現(xiàn),期待大佬們在評論區(qū)留言提供更好的解決方案。
補充
經(jīng)同事驗證,使用 .resx 格式的資源文件會更簡單一下,可以直接通過
BlackApp.ModuleA.Strings.zh_cn.ResourceManager("string1")
BlackApp.ModuleA.Strings.en_us.ResourceManager("string1")
的方式來訪問。但前提是需要將對應(yīng)資源文件的訪問修飾符設(shè)置為 public。
參考
* Localization of a WPF app - the simple approach
<https://codinginfinity.me/post/2015-05-10/localization_of_a_wpf_app_the_simple_approach>
* wpf-localization-multiple-resource-resx-one-language
<https://github.com/Jinjinov/wpf-localization-multiple-resource-resx-one-language>
* LocalizeMarkupExtension
<http://www.wpftutorial.net/LocalizeMarkupExtension.html>
* Markup Extensions and WPF XAML
<https://docs.microsoft.com/en-us/dotnet/framework/wpf/advanced/markup-extensions-and-wpf-xaml>
熱門工具 換一換