[UWP]本地化入門


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本身。

  1. 打開“添加新項”對話框,選中“資源文件(.resw)”,在“名稱”文本框中將文件名稱改為“Labels.resx”,點擊“添加”。
  2. 在“解決方案資源管理器”選中“Labels.resx”,郵件打開“屬性”視圖,“生成操作”選擇“嵌入的資源”。
  3. 將“Labels.resx”復制為“Labels.zh-CN.resx”,打開“Labels.zh-CN.resx”,“訪問修飾符”改為“無代碼生成”。
  4. 在“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

8. 源碼

GitHub - LocalizationDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM