1. 前言 "上一篇文章" 介紹了各種WPF本地化的入門知識,這篇文章介紹UWP本地化的入門知識。 2. 使用resw資源文件實現本地化 在以前的XAML平臺,resx資源文件是一種很方便的本地化方案,但在UWP中微軟又再次推薦x:Uid方案,預設的資源文件也變成resw資源文件。雖然尾碼名只差了一 ...
1. 前言
上一篇文章介紹了各種WPF本地化的入門知識,這篇文章介紹UWP本地化的入門知識。
2. 使用resw資源文件實現本地化
在以前的XAML平臺,resx資源文件是一種很方便的本地化方案,但在UWP中微軟又再次推薦x:Uid方案,預設的資源文件也變成resw資源文件。雖然尾碼名只差了一個字母,但使用方式完全不同。最主要的區別是resw資源文件不會創建對應的Designer.cs類,這就導致本地化的實現方案完全不同。
2.1 在XAML中實現本地化
在XAML中實現本地化的過程很簡單。首先在項目中新建"strings"文件夾,在"strings"文夾下創建"en-US"和"zh-CN"文件夾,併在兩個文件夾中分別添加"Resources.resw"資源文件。最終目錄結構如下:
在zh-CN\Resources.resw和en-US\Resources.resw添加兩個新資源,分別是UsernameTextBox.Width和UsernameTextBox.Header:
在XAML中添加一個TextBox,設置x:Uid為UsernameTextBox,x:Uid將XAML元素和資源文件中的資源進行關聯:
<TextBox x:Uid="UsernameTextBox"/>
運行後即可看到UsernameTextBox的Header設置為"用戶名",Width為100。
在“設置\區域和語言”中將"English"設置為預設語言,再次運行應用可看到運行在英語環境下的效果。
這樣基本的本地化功能就實現了。這種本地化方式有如下優點:
- 簡單快速,容易上手
- 語法簡單,不需要Binding等知識
- 可以指定任意屬性進行本地化
- 支持CLR屬性
除此之外,上一篇文章提到的ResXManager也支持Resw資源文件,還可以使用多語言應用工具包對資源文件進行管理,博客園的這篇文章頁對這個工具進行了詳細介紹:
Win10 UWP 開發系列:使用多語言工具包讓應用支持多語言
或者參考這個視頻:
Windows 10 Apps Designing for Global Customers
2.2 關聯到其它資源文件
UI元素預設與Resources.resw進行關聯,如果需要和其它資源文件關聯,可以加上資源文件的路徑。如需要與/OtherResources.resw中的資源關聯,x:Uid的語法如下:
x:Uid="/OtherResources/AddressTextBox"
2.3 附加屬性的本地化
對系統提供的附加屬性,資源的名稱語法如下:
UsernameTextBox.Grid.Row
對自定義附加屬性,語法稍微複雜一些:
ShowMessageButton.[using:LocalizationDemoUwp]ButtonEx.Content
奇怪的是,就這樣直接運行應用會報錯。只有應用這個資源的UI元素已經有這個附加屬性的值才能正常運行,簡單來說就是需要隨便為這個附加屬性設置一個值:
<Button Margin="5" x:Uid="ShowMessageButton" local:ButtonEx.Content="ssssss"/>
2.4 其它資源的本地化
除了字元串資源,其它資源的本地化方式不需要設置x:Uid,只需要建立對應語言的目錄結構及命名就可以在XAML中直接引用。如項目中有如下兩張圖片:
在XAML中可以直接通過Images/Flag.png
引用。路徑中的"zh-CN"、"en-US"稱為資源限定符,用於支持多種顯示比例、UI 語言、高對比度設置等,具體可參考Load images and assets tailored for scale, theme, high contrast, and others 。
2.5 在代碼里訪問資源
在代碼中訪問資源的代碼如下:
var resourceLoader = ResourceLoader.GetForCurrentView();
var currentLanguage = resourceLoader.GetString("CurrentLanguage");
resourceLoader = ResourceLoader.GetForCurrentView("OtherResources");
var message = resourceLoader.GetString("Message");
上面的代碼中,currentLanguage從預設的資源文件Resources.resw中獲取,resourceLoader 無需指定資源文件的名稱;而message 則從OtherResources.resw獲取,resourceLoader 需要指定資源文件的名稱。
如需要使用其它類庫中的資源,代碼如下:
resourceLoader = ResourceLoader.GetForCurrentView("LocalizationDemoUwp.ResourceLibrary/Resources");
currentLanguage = resourceLoader.GetString("CurrentLanguage");
雖然語法簡單,但可以看到最大的問題是資源的名稱沒有智能感知和錯誤提示,這樣使用資源很容易出錯。
如上圖所示,對錯誤的資源名稱,ReSharper會有錯誤提示,不過這種構造ResourceLoader的方式已經被標記為Deprecated並提示使用GetForCurrentView獲取ResourceLoader,而使用GetForCurrentView的情況下ReSharper又沒有錯誤提示。不知道ReSharper什麼時候才能支持在GetForCurrentView的方式下顯示錯誤提示(我安裝的ReSharper已是最新的2017.2)。
2.6 存在的問題
這個本地化方案雖然簡單,但我覺得很難使用,因為這個方案存在很多問題。
首先是設計時支持,對本地化來說,設計時支持主要包含3部分:
- 在編寫XAML時可以得到資源的智能感知
- 有完整的設計視圖
- 在不同語言之間切換
第一點,沒有,而且寫錯屬性名稱還不會在編譯時報錯,而是用最慘烈的方式呈現:運行時崩潰。
第二點,在Fall Creators Update (16299)以前,沒有,設計視圖一片空白。也可以隨便寫一些內容(如TextBox x:Uid="UsernameTextBox" Header="(here is header)"
)以輔助設計。但在XAML中寫的任何內容都可能被資源文件覆蓋,無論是文本還是大小、對齊方式或其它所有屬性對XAML的編寫者來說都是不可控的,不到實際運行時根本不清楚UI的最終效果,這就很考驗本地化人員和測試人員。在Fall Creators Update以後終於可以在設計視圖看到本地化的效果,這不得不說是巨大的進步。
第三點,目前來看做不到。
另外,資源管理也是個很麻煩的問題。同一個字元串,如果要對應TextBlock.Text、ContentControl.Content、TextBox.Header,這樣就需要三個資源,造成了冗餘,而大量的冗餘最終會導致錯誤。
總的來說,這個本地化方案有很多問題,雖然這個方案是微軟推薦的。既然是微軟推薦的,應該是支持最好的,也許是我的用法不對?
接下來在這個方案的基礎上做些改動,希望可以讓本地化更好用。
3. 動態切換語言
不是我太執著動態切換語言,是測試員真的喜歡這個功能,因為不用重啟應用就可以測試到所有語言的UI。
UWP提供了ApplicationLanguages.PrimaryLanguageOverride屬性用於更改語言首選項,即可以改變應用的語言,用法如下:
Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = "zh-CN";
這個變更是永久的,但不會對當前UI及一部分系統組件生效,只會影響之後創建的UI元素。更改ApplicationLanguages.PrimaryLanguageOverride,會非同步地觸發ResourceContext.QualifierValues的MapChanged事件,可以監聽這個事件並更新UI。這樣就可以實現簡單的動態切換語言功能。
DynamicResources.cs
public class DynamicResources : INotifyPropertyChanged
{
public DynamicResources()
{
_defaultContextForCurrentView = ResourceContext.GetForCurrentView();
_defaultContextForCurrentView.QualifierValues.MapChanged += async (s, m) =>
{
await MainPage.Current.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
OnPropertyChanged("");
});
};
}
private ResourceContext _defaultContextForCurrentView;
public string Main
{
get { return ResourceManager.Current.MainResourceMap.GetValue("DynamicResources/Main", _defaultContextForCurrentView).ValueAsString; }
}
public string Settings
{
get { return ResourceManager.Current.MainResourceMap.GetValue("DynamicResources/Settings", _defaultContextForCurrentView).ValueAsString; }
}
public string RestartNote
{
get { return ResourceManager.Current.MainResourceMap.GetValue("DynamicResources/RestartNote", _defaultContextForCurrentView).ValueAsString; }
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
SettingView.xaml
<Page.Resources>
<local:DynamicResources x:Key="DynamicResources"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<ListView x:Name="LanguageListView" Margin="10">
<ListViewItem Tag="zh-Hans-CN" Content="中文"/>
<ListViewItem Tag="en-US" Content="English"/>
</ListView>
<TextBlock x:Name="NoteElement" Foreground="#FFF99F00" Margin="20,10" Visibility="Collapsed"
Text="{Binding RestartNote,Source={StaticResource DynamicResources}}"
/>
</StackPanel>
</Grid>
SettingView.xaml.cs
private async void OnLanguageListViewSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var item = LanguageListView.SelectedItem as ListViewItem;
if (item == null)
return;
ApplicationLanguages.PrimaryLanguageOverride = item.Tag as string;
_hasChangedLanguage = true;
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, ShowNoteElement);
}
private void ShowNoteElement()
{
NoteElement.Visibility = Visibility.Visible;
var appView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView();
appView.Title = (LanguageListView.SelectedItem as ListViewItem)?.Content as string;
}
只在設置頁面及菜單這些在切換語言時不會重新載入的UI上使用Binding,其它地方不變,這樣簡單的動態切換語言就實現了。運行結果如上,可以看到TextBox右鍵菜單仍未切換語言,需要重新啟動。
UWP預設只安裝電腦對應的語言,這樣可以節省安裝空間,但影響到動態切換語言的功能,要解決這個問題可以參考以下內容(我沒有驗證過):localization - How to always install all localized resources in Windows Store UWP app - Stack Overflow 。
4. 獲得完整的設計視圖
在Fall Creators Update以前為了獲得設計時視圖可以使用索引器。很少有機會在C#中用到索引器,XAML中也很少用到Binding到字元串索引的語法,就是這兩個功能在本地化中幫了大忙。
public class ResourcesStrings
{
public string this[string key]
{
get
{
return ResourceLoader.GetForViewIndependentUse().GetString(key);
}
}
}
<Page.Resources>
<local:ResourcesStrings x:Key="S"/>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<TextBlock Text="{Binding Source={StaticResource S},Path=[MainTitle]}" />
</Grid>
只需要這樣寫就可以獲得完整的設計時試圖,可是還是沒有解決智能感知和錯誤提示這兩個問題。
在這個方案上也可簡單地實現動態切換語言。
public class ApplicationResources : INotifyPropertyChanged
{
public ApplicationResources()
{
DynamicResources = new DynamicResourcesStrings();
Resources = new ResourcesStrings();
Current = this;
}
public static ApplicationResources Current { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
public DynamicResourcesStrings DynamicResources { get; }
public ResourcesStrings Resources { get; }
public string Language
{
get
{
return ApplicationLanguages.PrimaryLanguageOverride;
}
set
{
if (ApplicationLanguages.PrimaryLanguageOverride == value)
return;
ApplicationLanguages.PrimaryLanguageOverride = value;
if (MainPage.Current != null )
MainPage.Current.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => { OnPropertyChanged(""); });
}
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
<ListViewItem Content="{Binding Source={StaticResource R},Path=DynamicResources[Main]}"/>
不知道為什麼,在VisualStudio上有時沒辦法獲得設計時視圖,所有文字都顯示為"Item"。
5. 使用resx資源文件
既然UWP是XAML大家族的一份子,那麼應該也可以使用resx資源文件實現本地化,畢竟生成resx對應代碼的是PublicResXFileCodeGenerator,而不是UWP本身。
- 打開“添加新項”對話框,選中“資源文件(.resw)”,在“名稱”文本框中將文件名稱改為“Labels.resx”,點擊“添加”。
- 在“解決方案資源管理器”選中“Labels.resx”,郵件打開“屬性”視圖,“生成操作”選擇“嵌入的資源”。
- 將“Labels.resx”複製為“Labels.zh-CN.resx”,打開“Labels.zh-CN.resx”,“訪問修飾符”改為“無代碼生成”。
在“AssemblyInfo.cs”添加如下代碼:
[assembly: NeutralResourcesLanguage("en-US")]
這樣就可以在UWP中使用resx資源文件了。實現本地化的代碼和上一篇文章中介紹的WPF本地化方案差不多。
public class ApplicationResources : INotifyPropertyChanged
{
public static ApplicationResources Current { get; private set; }
public ApplicationResources()
{
Labels = new Labels();
if (string.IsNullOrWhiteSpace(ApplicationLanguages.PrimaryLanguageOverride) == false)
Language = ApplicationLanguages.PrimaryLanguageOverride;
else
Language = Windows.System.UserProfile.GlobalizationPreferences.Languages.FirstOrDefault();
Current = this;
}
public Labels Labels { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private string _language;
/// <summary>
/// 獲取或設置 Language 的值
/// </summary>
public string Language
{
get { return _language; }
set
{
if (_language == value)
return;
_language = value;
Labels.Culture = new System.Globalization.CultureInfo(_language);
ApplicationLanguages.PrimaryLanguageOverride = _language;
OnPropertyChanged("");
}
}
}
使用體驗和WPF中的resx本地化方案差不多,設計時支持幾乎完美,包括智能感知和錯誤提示,不過還是沒辦法解決系統組件中的本地化問題(如TextBox右鍵菜單)。另外,編譯時會報錯:帶有輸出類型“appcontainerexe”的項目不支持生成操作“EmbeddedResource”。解決方案是不在UWP應用項目中添加resx資源文件,而在類庫中添加resx資源文件,這樣連錯誤都不報了。
不知道Xamarin.Forms是不是也可以這樣實現,畢竟它也是XAML大家族的一員。
6. 結語
研究了這麼多resw資源文件的方案,結果還是resx資源文件用得最順手,畢竟這個方案我已經用了很多年(在silverlight中只能用這個方案)。具體使用哪個方案見仁見智。
需要強調的是resx並不能完全替代resw方案,很多時候需要混合使用,例如應用的Display Name可以使用resw輕鬆實現本地化:
本地化的主題仍有很多內容,這篇文章只打算介紹入門知識,更深入的知識可以參考下麵給出的鏈接。
7. 參考
Guidelines for globalization - UWP app developer Microsoft Docs
Localize strings in your UI and app package manifest - UWP app developer Microsoft Docs
Load images and assets tailored for scale, theme, high contrast, and others - UWP app developer Microsoft Docs
快速入門:翻譯 UI 資源 (XAML)
c# - UWP Resource file for languages is not deployed correctly - Stack Overflow
localization - How to always install all localized resources in Windows Store UWP app - Stack Overflow
Win10 UWP 開發系列:使用多語言工具包讓應用支持多語言 - yan_xiaodi - 博客園
Windows 10 Apps Designing for Global Customers