之前我在一篇blog中寫過如何使用多語言工具包,見http://www.cnblogs.com/yanxiaodi/p/3800767.html在WinEcos社區也發佈過一篇詳細的文章介紹多語言工具包的使用,但因社區改版那篇文章已經找不到了。當時寫的時候還沒有出Win10的SDK,都是基於UAP框...
之前我在一篇blog中寫過如何使用多語言工具包,見http://www.cnblogs.com/yanxiaodi/p/3800767.html
在WinEcos社區也發佈過一篇詳細的文章介紹多語言工具包的使用,但因社區改版那篇文章已經找不到了。
當時寫的時候還沒有出Win10的SDK,都是基於UAP框架寫的。微軟早已經發佈了Win10的SDK,相應的項目結構也發生了變化,以前分為兩個項目通過Share項目共用代碼的方式被拋棄,改為合併為一個項目,真正實現了一套代碼相容PC和Mobile兩個平臺,我已經基於Win10 10586的SDK發佈了Currency Exchanger的新版本,下載地址:https://www.microsoft.com/store/apps/9WZDNCRDQ91S
在開發Currency Exchanger的過程中,我又整理了一下支持多語言的問題,記錄於此。
一、安裝多語言工具包
使用VS2015開發UWP不能再使用老版3.0的多語言工具包,而應該使用新版的V4.0beta,這個還不是正式版,所以相容性有問題,無法與V3.0共存,安裝之後也無法再用VS2013或VS2015打開WP8.1之前的項目,所以安裝之前請慎重!請慎重!請慎重!重要的事情說三遍。
我們在開發者後臺的下載欄目可以找到多語言工具包的下載頁面:https://dev.windows.com/zh-cn/develop/multilingual-app-toolkit
但是!截至到2015年12月31日,這個頁面所下載的中文多語言工具包仍然是V3.0,而不是最新的V4.0beta,就算安裝了,也無法在UWP項目中應用。
最新版的下載地址在此:
https://visualstudiogallery.msdn.microsoft.com/6dab9154-a7e1-46e4-bbfa-18b5e81df520
這也是我一直吐槽MSDN的原因之一,找個東西累死了,官方的東西都不好找。
還有一種方式,直接在VS2015的擴展里搜索Multilingual App Toolkit,主要要有空格,不然搜不到:
要安裝V4.0beta這個。下麵那個是舊版的,這兩個無法共存。
二、啟用多語言工具包
還是做個例子吧。新建一個MultilingualDemo項目,VS2015工具菜單-Multilingual App Toolkit -啟用選定內容
會收到提示:1> 未啟用項目"MultilingualDemo"-沒有可本地化的資源被髮現。
這是因為沒有發現咳本地化的資源,雙擊Package.appxmanifest打開,設置一個預設語言,如果在設計的時候就想支持多語言,最好預設語言設置為英語,輸入en-US:
然後在項目中添加一個Strings文件夾,再在其下添加一個en-US文件夾,這個文件夾名字要和預設語言代碼保持一致,如果預設語言是zh-CN,那就建一個zh-CN的文件夾。
在這個目錄下添加一個Resources.resw資源文件,在這裡面編輯所需要的字元串:
添加幾個資源,註意,如果是要顯示在界面上的,可以根據控制項的屬性來設置,如TextBlock的文字是Text屬性,那資源的名字就命名為HelloWorld.Text,Button的文字是Content屬性,所以命名為ClickMe.Content,另外我還加了一個AppName,用於在代碼中使用。
再次 VS2015工具菜單-Multilingual App Toolkit -啟用選定內容
這次可以正常啟用了:
1> 項目"MultilingualDemo"已啟用。 該項目的來源,文化是 'en-US' [英語(美國)]。
三、在XAML界面上使用語言資源
在Page中放一個 TextBlock,一個Button,一個ComboBox,設置其x:Uid(資源標識符,註意不是x:Name)屬性,這樣控制項就可以根據資源找到其對應的內容:
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" DataContext="{StaticResource DesignVM}">
<TextBlock x:Name="pageTitle" Grid.Column="1" Margin="100" Text="{Binding Title}" />
<TextBlock x:Name="textBlock" x:Uid="HelloWorld" HorizontalAlignment="Left" Margin="100,156,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
<Button x:Name="button" x:Uid="ClickMe" HorizontalAlignment="Left" Margin="100,181,0,0" VerticalAlignment="Top"/>
<ComboBox x:Name="comboBox" x:Uid="ChangeLanguage" HorizontalAlignment="Left" Margin="100,249,0,0" VerticalAlignment="Top" Width="120"/>
</Grid>
四、在代碼中使用語言資源
添加一個
public static class AppResources
{
private static ResourceLoader CurrentResourceLoader
{
get { return _loader ?? (_loader = ResourceLoader.GetForCurrentView("Resources")); }
}
private static ResourceLoader _loader;
private static readonly Dictionary<string, string> ResourceCache = new Dictionary<string, string>();
public static string GetString(string key)
{
string s;
if (ResourceCache.TryGetValue(key, out s))
{
return s;
}
else
{
s = CurrentResourceLoader.GetString(key);
ResourceCache[key] = s;
return s;
}
}
/// <summary>
/// AppName
/// </summary>
public static string AppName
{
get
{
return CurrentResourceLoader.GetString("AppName");
}
}
}
打開MainPage_Model.cs,取消OnBindedViewLoad方法的註釋,在裡面添加以下代碼:
/// <summary>
/// This will be invoked by view when the view fires Load event and this viewmodel instance is already in view's ViewModel property
/// </summary>
/// <param name="view">View that firing Load event</param>
/// <returns>Task awaiter</returns>
protected override Task OnBindedViewLoad(MVVMSidekick.Views.IView view)
{
this.Title = AppResources.AppName;
return base.OnBindedViewLoad(view);
}
現在運行看看:
現在預設語言是en-US。
五、翻譯成本地化資源
如果沒安裝多語言工具包的話,可以在Strings目錄下手動添加對應語言的文件夾和資源文件,不過有多語言工具包的話這個工作就變得很容易了,在項目上右鍵,添加翻譯語言:
在這裡可以選擇要添加什麼語言,選擇簡體中文:
這裡建議只選擇zh-Hans就可以了,不用選擇zh-CN,因為語言與資源的匹配非常複雜,語言標記存在多種可能影響匹配優先順序的可選組件,建議讓系統來選擇,MSDN文檔是這麼說的:(https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/mt607079.aspx)
使用某個語言標記的可選組件的示例有:
- 用於禁止腳本語言的腳本。例如,en-Latn-US 與 en-US 匹配。
- 區域。例如,en-US 與 en 匹配。
- 變體。例如,de-DE-1996 與 de-DE 匹配。
- -x 和其他擴展名。例如,en-US-x-Pirate 與 en-US 匹配。
對於不採用 xx 或 xx-yy 形式的語言標記,也存在許多組件,且並非全部匹配。
- zh-Hant 與 zh-Hans 不匹配。
Windows 以一個標準的易於理解的方式排定語言匹配的優先順序。例如,按優先順序,en-US 依次與 en-US、en、en-GB 等等匹配。
- Windows 執行跨區域匹配。例如,en-US 與 en-US 匹配,然後依次與 en、en-* 匹配。
- Windows 提供了一些額外數據,它們可用於區域內(如某種語言的主要區域)的相關性匹配。例如,fr-FR 比 fr-CA 更匹配 fr-BE。
- 如果你使用 Windows API,則可以免費獲取日後 Windows 在語言匹配方面的任何改進。
在與列表中的首個語言匹配之後才會與列表中第二個語言匹配,對於其他區域變體也是如此。例如,如果應用程式語言為 en-US,則會先於 fr-CA 資源選擇用於 en-GB 的資源。僅當沒有用於 en 形式的資源時才選擇用於 fr-CA 的資源。
應用程式語言列表設置為用戶的區域變體,儘管該變體不同於應用提供的區域變體。例如,如果用戶使用 en-GB,但應用支持 en-US,則應用程式語言列表將包含 en-GB。這將確保日期、時間和數字的格式更接近用戶的期望 (en-GB),但仍然使用應用支持的語言 (en-US) 載入 UI 資源(由於語言匹配)。
除非想把本地化做的非常完善,為中國用戶和新加坡用戶都提供不同的語言資源,否則只提供一種中文簡體就夠了。
選擇後,Strings目錄下會自動添加中文簡體資源文件:
但是中文的資源里還是空的,我們需要翻譯一下英文資源。右鍵單擊MultilingualDemo.zh-Hans.xlf,選擇打開方式:
現在可以輸入翻譯了,如果懶的話就點擊菜單翻譯按鈕調用Bing的翻譯介面自動翻譯一下,保存。
重新編譯生成一下項目,多語言工具包會根據預設資源去填充其他語言的資源文件:
現在可以看到中文的資源文件里也已經有了。翻譯的時候要註意,字元串有幾種狀態,新、翻譯、需要評審、最終等,可以根據這幾種狀態靈活切換顯示哪些字元串來處理。
重新運行項目,如果我們的電腦系統預設是中文語言,那app應該已經變成中文界面了。如果用戶電腦或手機預設語言是英語,則會調用en-US,如果是其他語言,則會調用預設語言en-US。
六、更改首選語言
App應具有可更改語言的設置。為了保存用戶的首選語言,需要使用Windows.Storage.ApplicationData.Current.LocalSettings來保存用戶的設置,關於如何使用這個來保存配置網上有很多介紹,這裡就不詳細介紹了。
在MainPage.xaml裡頭部添加以下命名空間:
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
把ComboBox代碼改成這樣:
<ComboBox x:Name="comboBox" x:Uid="ChangeLanguage" HorizontalAlignment="Left" Margin="100,249,0,0" VerticalAlignment="Top" Width="120"
ItemsSource="{Binding LanguageList}" SelectedIndex="{Binding LanguageCodeIndex, Mode=TwoWay}" >
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
<Interactivity:Interaction.Behaviors>
<Core:EventTriggerBehavior EventName="SelectionChanged">
<Core:InvokeCommandAction Command="{Binding CommandLanguageChanged}" CommandParameter="{Binding LanguageCodeIndex}"/>
</Core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</ComboBox>
這裡我們主要使用了Windows.Globalization裡面的類,Windows.Globalization 還具有作為幫助程式對象提供的 Language 對象。它幫助應用檢查有關語言的詳細信息,例如,語言的腳本、顯示名稱和本地名稱。主要語言替代PrimaryLanguageOverride是一個簡單的替代設置,它用於讓用戶獨立選擇語言的應用,或者有充分理由替代預設語言選擇的應用。
相應的vm里添加以下代碼:
public ObservableCollection<Language> LanguageList
{
get { return _LanguageListLocator(this).Value; }
set { _LanguageListLocator(this).SetValueAndTryNotify(value); }
}
#region Property ObservableCollection<Language> LanguageList Setup
protected Property<ObservableCollection<Language>> _LanguageList = new Property<ObservableCollection<Language>> { LocatorFunc = _LanguageListLocator };
static Func<BindableBase, ValueContainer<ObservableCollection<Language>>> _LanguageListLocator = RegisterContainerLocator<ObservableCollection<Language>>("LanguageList", model => model.Initialize("LanguageList", ref model._LanguageList, ref _LanguageListLocator, _LanguageListDefaultValueFactory));
static Func<ObservableCollection<Language>> _LanguageListDefaultValueFactory = () => { return new ObservableCollection<Language>(); };
#endregion
/// <summary>
/// 語言設置
/// </summary>
public int LanguageCodeIndex
{
get { return _LanguageCodeIndexLocator(this).Value; }
set { _LanguageCodeIndexLocator(this).SetValueAndTryNotify(value); }
}
#region Property int LanguageCodeIndex Setup
protected Property<int> _LanguageCodeIndex = new Property<int> { LocatorFunc = _LanguageCodeIndexLocator };
static Func<BindableBase, ValueContainer<int>> _LanguageCodeIndexLocator = RegisterContainerLocator<int>("LanguageCodeIndex", model => model.Initialize("LanguageCodeIndex", ref model._LanguageCodeIndex, ref _LanguageCodeIndexLocator, _LanguageCodeIndexDefaultValueFactory));
static Func<int> _LanguageCodeIndexDefaultValueFactory = () => { return -1; };
#endregion
public CommandModel<ReactiveCommand, String> CommandLanguageChanged
{
get { return _CommandLanguageChangedLocator(this).Value; }
set { _CommandLanguageChangedLocator(this).SetValueAndTryNotify(value); }
}
#region Property CommandModel<ReactiveCommand, String> CommandLanguageChanged Setup
protected Property<CommandModel<ReactiveCommand, String>> _CommandLanguageChanged = new Property<CommandModel<ReactiveCommand, String>> { LocatorFunc = _CommandLanguageChangedLocator };
static Func<BindableBase, ValueContainer<CommandModel<ReactiveCommand, String>>> _CommandLanguageChangedLocator = RegisterContainerLocator<CommandModel<ReactiveCommand, String>>("CommandLanguageChanged", model => model.Initialize("CommandLanguageChanged", ref model._CommandLanguageChanged, ref _CommandLanguageChangedLocator, _CommandLanguageChangedDefaultValueFactory));
static Func<BindableBase, CommandModel<ReactiveCommand, String>> _CommandLanguageChangedDefaultValueFactory =
model =>
{
var resource = "CommandLanguageChanged"; // Command resource
var commandId = "CommandLanguageChanged";
var vm = CastToCurrentType(model);
var cmd = new ReactiveCommand(canExecute: true) { ViewModel = model }; //New Command Core
cmd.DoExecuteUIBusyTask(
vm,
async e =>
{
//Todo: Add LanguageChanged logic here, or
await MVVMSidekick.Utilities.TaskExHelper.Yield();
string oldLan = AppSettings.Instance.LanguageCode;
if (vm.LanguageCodeIndex >= 0)
{
var lan = vm.LanguageList[vm.LanguageCodeIndex];
AppSettings.Instance.LanguageCode = lan.LanguageTag;
ApplicationLanguages.PrimaryLanguageOverride = lan.LanguageTag;
}
})
.DoNotifyDefaultEventRouter(vm, commandId)
.Subscribe()
.DisposeWith(vm);
var cmdmdl = cmd.CreateCommandModel(resource);
cmdmdl.ListenToIsUIBusy(
model: vm,
canExecuteWhenBusy: false);
return cmdmdl;
};
#endregion
屬性可以用propvm代碼段,命令用propcmd代碼段來快速生成。
在MainPage的vm的load事件中初始化語言列表:
if (!LanguageList.Any())
{
var lanList = ApplicationLanguages.ManifestLanguages;
foreach (var lan in lanList)
{
LanguageList.Add(new Language(lan));
}
}
if (!string.IsNullOrEmpty(AppSettings.Instance.LanguageCode))
{
Language userLan = LanguageList.FirstOrDefault(x => x.LanguageTag == AppSettings.Instance.LanguageCode);
LanguageCodeIndex = LanguageList.IndexOf(userLan);
}
如果用戶更改語言後,在程式載入時應該按照用戶選擇的語言來調用,打開App.xaml.cs,在App構造 函數中添加以下代碼:
public App()
{
//TODO 這裡可以根據用戶需要更改語言
if (!string.IsNullOrEmpty(AppSettings.Instance.LanguageCode))
{
ApplicationLanguages.PrimaryLanguageOverride = AppSettings.Instance.LanguageCode;
}
//ApplicationLanguages.PrimaryLanguageOverride = "cs";
//ResourceContext.GetForCurrentView().Reset();
this.InitializeComponent();
this.Suspending += OnSuspending;
}
現在一個具有基本多語言支持、可更改語言的app就完成了。用戶選擇不同的語言後,重新打開就會重新設置語言。
七、其他
這裡是微軟官方的一個例子,不過是win8平臺的,可以參考:https://code.msdn.microsoft.com/windowsapps/Application-resources-and-cd0c6eaa/
還有一些特殊的情況需要考慮,如果非要做的那麼完美的話:
- 有可能有的語言字元數比較多,導致控制項寬度不夠,這就需要為每種語言設置控制項的寬度,比如創建App_Name.Width的資源;
- 有可能語言的排列方向不一致,比如阿拉伯語是右對齊,可以設置對齊屬性;
- 有可能圖像也需要本地化,那就需要按照資源限定符的格式來定義圖片路徑:
標準命名約定為foldername/qualifiername-value_qualifiername-value/filename.qualifiername-value_qualifiername-value.ext,當資源路徑為Images/en-US/homeregion-USA/logo.scale-100_contrast-white.png時,應以Images/logo.png的方式來載入。
關於如何使用資源限定符,可參考:https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/hh965324.aspx
這個頁面是一些本地化的最佳實踐:https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/hh967762.aspx
CurrencyExchanger的本地化我也沒有做的那麼完善,基本就是只翻譯了語言,沒有考慮寬度啊排列方式這些,工作量太大了。
至於如何翻譯這些文字,建議在app里留一個郵件,讓志願者來幫助翻譯,可以將xlf文件上右鍵導出翻譯,選擇xlf格式或者csv格式,發送給翻譯人員翻譯,然後再導入進來即可。但這裡又會遇到一個問題,導出的csv用excel打開的話,再另存,會丟失裡面的雙引號。
從xlf文件導出的csv格式是這樣的:
除了表頭,每個欄位兩邊都有雙引號。
這個文件用excel打開再另存後,再用文本編輯軟體打開,會變成這樣:
兩邊的引號沒有了,導入的時候會提示錯誤,無法導入。
我想了個笨辦法,在excel里編輯csv的時候,修改單元格屬性,改為 !"@!" 然後再保存,這樣在文本編輯軟體里打開會發現一個雙引號變成了兩個,然後再使用批量替換功能,把"""替換為",同時別忘了把第一行表頭的雙引號去掉,才能正確導入。
其實如果直接把資源文件里的內容複製到excel里發送給翻譯人員翻譯,翻譯好了再粘貼回來也行,但要是以後再增加修改預設語言的資源時,其他語言也得手動挨個改,不如用多語言工具包自動填充完整。各有利弊。
最後附上demo下載地址:
鏈接:http://pan.baidu.com/s/1i4jBn8L 密碼:idpz