軟件國際化是在軟件設計和文檔開發過程中,使得功能和代碼設計能處理多種語言和文化習俗,在創建不同語言版本時,不需要重新設計源程序代碼的軟件工程方法。這在很多成熟的軟件開發平台中非常常見。對於.net開發者來說,我們一般可以通過以下兩種方式來實現軟件的國際化。
- 語言配置文件
- 資源文件
在.net平台中,軟件的國際化主要依靠工作線程的國際化來完成。在.net框架的的處理線程中,我們通過設置Thread.CurrentCulture屬性來實現對日期、時間、數字、貨幣值、文本的排序順序,負載約定和字符串比較的默認值的格式確定,默認情況下,這個屬性來自於“控制面板”的“區域和語言選項”中的用戶區域性。當然,在軟件運行過程中也可以通過手動的方式強制改變Thread.CurrentCulture屬性值。CurrentUICulture屬性則用來確定需要向用戶呈現的資源格式,它對軟件的操作界面來說最有用,因為它標識了在顯示UI元素時應使用的語言。在.net中通常給軟件設置不同的UI資源文件,使得軟件運行時通過CurrentUICulture屬性值來選擇不同語言的資源文件渲染軟件界面。線程的CurrentUICulture 和 CurrentCulture 屬性一般設置為同一個CultureInfo對象,也就是就他它使用相同的語言、國家信息,然而也可將它們設為不同的對象。例如一個美國人在北京借用了一台操作系統是簡體中文版的計算機進行工作時,就可以通過這種方式讓軟件滿足美國用戶的使用需求。
下面的例子解釋了對於不同的Thread.CurrentCulture屬性值,同一個日期字符串轉換為日期類型時會生成不同結果
1 static void Main(string[] args) 2 { 3 string birthdate = "02/06/2013"; 4 DateTime dateTime; 5 dateTime = DateTime.Parse(birthdate); 6 Console.WriteLine(dateTime.ToString("yyyy年MM月dd日")); 7 Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-GB"); 8 dateTime = DateTime.Parse(birthdate); 9 Console.WriteLine(dateTime.ToString("yyyy年MM月dd日")); 10 Console.Read(); 11 }
下面的例子解釋了對於不同的Thread.CurrentUICulture屬性值,框架會選擇不同的資源文件讀取其中的值,以便向使用不同語言的用戶呈現不同的信息。運行前,需要在項目中建立如下的資源文件。
每個資源文件的內容如下:
其中,文件名中的語言代碼代表不同的國家及使用的語言,如en-US=美國英語、en-GB=英國英語等等。文件名不帶語言代碼的為默認資源文件,框架會根據當前計算機所在的區域來調用這個資源文件。由於我在中國,並且使用簡體中文的操作系統,所以,會將默認資源文件中的中文字符讀出。
1 static void Main(string[] args) 2 { 3 string hello; 4 hello = International.Hello; 5 Console.WriteLine(hello); 6 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("ja-JP"); 7 hello = International.Hello; 8 Console.WriteLine(hello); 9 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US"); 10 hello = International.Hello; 11 Console.WriteLine(hello); 12 Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-GB"); 13 hello = International.Hello; 14 Console.WriteLine(hello); 15 Console.Read(); 16 }
那么在Web API中,如何確定用戶來自哪個國家以及使用哪種語言、文化呢?由於Web API無法主動讀取用戶所有區域的信息,那么就只能被動地從用戶那里獲取此類信息。而這部分信息則會被包含在用戶提交的HTTP請求頭信息中。比如:
Accept-Language: en-us, en-gb;q=0.8, en;q=0.7
在這段HTTP請求頭信息中,所傳達的信息就是用戶可接受的語言和地區文化。每種國家及語言后面的參數q稱為相對質量的因素(relative quality factor),代表用戶對於該種語言可接受的優先度(取值0.0~1.0,默認值為1.0)。總之通俗一來就講,上面這段頭信息的意思就是用戶首選是美式英語,如果不支持的話,沒關系,那就英式英語吧,再不行的話,其它類型的英語也可以。
上面的這段請求頭信息提交到服務器時,會被封裝在HttpRequestMessage類(System.Net.Http命名空間)的Headers屬性中,我們可以很輕易地在Web API的控制器中讀取到。參考以下例子
1 public void Post(Object obj) 2 { 3 HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptedLanguages = Request.Headers.AcceptLanguage; 4 foreach (StringWithQualityHeaderValue language in acceptedLanguages) 5 { 6 Debug.WriteLine(language.Value); 7 Debug.WriteLine(language.Quality); 8 } 9 }
當然,在實際的開發中,上面的方法不是推薦的方法。更加科學的方法是我們新建一個自定義的DelegatingHandler,簡單一點來講,DelegatingHandler是HTTP請求通道中的過濾器。我們可以在這個DelegatingHandler中讀取Accept-Language信息,並且設置處理線程的Thread.CurrentCulture屬性和CurrentUICulture屬性。源碼如下:
1 namespace HelloWebAPI.Infrastructure 2 { 3 public class CultureHandler : DelegatingHandler 4 { 5 6 private List<string> supportedCulture = new List<string>() 7 { 8 "zh-cn", "en-us", "ja-jp" 9 }; 10 11 protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) 12 { 13 HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptedLanguage = request.Headers.AcceptLanguage; 14 if (acceptedLanguage != null && acceptedLanguage.Count > 0) 15 { 16 StringWithQualityHeaderValue preferredLanguage = 17 acceptedLanguage.OrderByDescending(e => e.Quality ?? 1.0D) 18 .Where(e => !e.Quality.HasValue || e.Quality.Value > 0.0D) 19 .FirstOrDefault( 20 e => supportedCulture.Contains(e.Value, StringComparer.OrdinalIgnoreCase)); 21 if (preferredLanguage != null) 22 { 23 // 如需要,此處也可同時設置CurrentCulture屬性 24 Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(preferredLanguage.Value); 25 } 26 27 if (acceptedLanguage.Any(e => e.Value == "*" &&(!e.Quality.HasValue || e.Quality.Value > 0.0D))) 28 { 29 string selectedCulture = 30 supportedCulture.FirstOrDefault(e => !acceptedLanguage.Any( 31 ee => 32 ee.Value.Equals(e, StringComparison.OrdinalIgnoreCase) && ee.Quality.HasValue && 33 ee.Quality.Value == 0.0D)); 34 if (!string.IsNullOrWhiteSpace(selectedCulture)) 35 { 36 // 如需要,此處也可同時設置CurrentCulture屬性 37 Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(selectedCulture); 38 } 39 } 40 } 41 return base.SendAsync(request, cancellationToken); 42 } 43 } 44 }
上面的這段代碼中,假設系統支持的語言有美式英語、簡體中文、日文。並且如果用戶支持多種語言,該DelegatingHandler也會根據頭信息中的相對質量的因素q進行排序以確定首選語言。
打開WebApiConfig.cs文件,注冊自定義的DelegatingHandler,在Register靜態方法中添加如下的語句。
1 public static void Register(HttpConfiguration config) 2 { 3 config.Routes.MapHttpRoute( 4 name: "DefaultApi", 5 routeTemplate: "api/{controller}/{id}", 6 defaults: new { id = RouteParameter.Optional } 7 ); 8 config.MessageHandlers.Add(new CultureHandler()); 9 }
應用場景一:根據用戶所在的國家和使用的語言,向用戶提供不同語言的響應
在項目中,新建一個App_GlobalResources文件夾,該文件為ASP.net的專屬文件夾,專門用於存放資源文件。分別建立幾個對應語言的資源文件。文件中均有一個同鍵不同值的字符串“NotFound”,該字符串用於提示用戶找不到指定的文件或資源。
在控制器中,建立一個示例動作方法如下:
1 public class EmployeesController : ApiController 2 { 3 public HttpResponseMessage Get(int id) 4 { 5 // 業務邏輯,此處省略 6 return Request.CreateErrorResponse(HttpStatusCode.NotFound, Resources.Message.NotFound); 7 } 8 }
為了調試,我們使用Fiddler來進行,運行項目之后,我們通過設置HTTP請求頭信息中的Accept-Lanuage,可以得到不同語言的響應。
應用場景二:根據用戶所在的國家和使用的語言,對用戶提交的數據進行處理和提交。
回想文章開頭的例子,對於一個相同的時間日期字符串,不同國家的用戶可能會有不同的理解。比如"05/06/2013",類似於這種格式的日期在計算機中很常見。但恰恰是這樣一種表達方式在美國人看來是2013年5月6日,因為美國習慣於MM/dd/yyyy這種日期格式;但在英國人看來則會是2013年6月5日,因為他們所理解的日期格式是 dd/MM/yyyy。因此,對於Web API來說,如果我們面對的文化背景如此復雜的用戶,那么在處理用戶的數據時不得不倍加細心,否則將會造成難以挽回的損失。如何應對這個問題呢?
首先要知道,在Web API框架中,關於JSON的序列化和反序列化,微軟已經將這個任務委托給第三方類庫JSON.net。經過查閱文檔,發現JSON.net對於日期格式都使用了日期轉換器進行轉換,並且內置了兩個常用的日期轉換器,JavaScriptDateTimeConverter和IsoDateTimeConverter,這兩個日期轉換器運行時,通過讀取SerializerSettings類的Culture屬性來設置線程的Culture。
所以,我們通過設置JSON.net的SerializerSettings類中的Culture屬性也可以實現國際化。因此,在WebApiConfig.cs文件中,我們可以直接對其設置。
1 public static void Register(HttpConfiguration config) 2 { 3 config.Routes.MapHttpRoute( 4 name: "DefaultApi", 5 routeTemplate: "api/{controller}/{id}", 6 defaults: new { id = RouteParameter.Optional } 7 ); 8 config.Formatters.JsonFormatter.SerializerSettings.Culture = new System.Globalization.CultureInfo("en-US"); 9 }
但是,這根本不是最好的解決方案。因為它沒辦法根據HTTP請求頭信息來設置Culture屬性,WebApiConfig類只是一個配置類,不是一個過濾器,它沒辦法訪問HTTP請求。
所以,問題的最終解決方案是我們應該避開SerializerSettings類,自己編寫DataTimeConverter類對日期進行轉換,設置線程的Cultrue工作交由之前自定義的CultureHandler去完成。
DataTimeConverter源代碼如下:
1 namespace HelloWebAPI.Infrastructure 2 { 3 public class DateTimeConverter : DateTimeConverterBase 4 { 5 public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 6 { 7 writer.WriteValue(((DateTime)value).ToString()); 8 } 9 10 public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 11 { 12 return DateTime.Parse(reader.Value.ToString()); 13 } 14 } 15 }
同時,我們還需要將CultureHandler源碼中的Thread.CurrentThread.CurrentUICulture改為Thread.CurrentThread.CurrentCulture。
最后,我們需要在WebApiConfig.cs中對日期轉換器進行注冊,為了避免與框架中內置的DateTimeConverter沖突,此處使用了類的完全限定名。
1 public static void Register(HttpConfiguration config) 2 { 3 config.Routes.MapHttpRoute( 4 name: "DefaultApi", 5 routeTemplate: "api/{controller}/{id}", 6 defaults: new { id = RouteParameter.Optional } 7 ); 8 config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new HelloWebAPI.Infrastructure.DateTimeConverter()); 9 config.MessageHandlers.Add(new CultureHandler()); 10 }
啟動項目,同時使用Fiddler提交示例的JSON數據如下:
{id:123,firstName:"Gates", lastName:"Bill", age:58, birthdate:"05/06/1955"}
分別設置不同的Accept-Language頭信息進行調試