[.NET]簡單實作多國語言的基底-使用Resource資源檔
前言
在與同事討論程式碼的過程中,提到了magic string這個term。
magic string簡單的說,就是程式碼裡面出現hard-code的字串,而通常都是屬於『訊息』類的,這樣的字串既不好修改,也容易讓人不了解怎麼會突然間冒出這樣的字串出來。而訊息通常還要考慮到i18n與需要容易修改、抽換等情況…
這篇文章,就透過.NET的.resx資源檔,來解決magic string與多國語言的問題。
簡要說明
在.NET中,可以透過resource檔來放不同文化特性的資源檔,輕易地設計出具有彈性的程式碼。
- 透過ResourceManager來存取不同文化語系的資源檔。
- 程式碼邏輯與資源檔獨立開來。
- 程式碼中透過resource檔中存放的NameValueCollection的Name來讀取Value,不會再出現magic string。
實作
首先建立一個Library,裡面新增兩個資源檔:分別為MessageFile.en-US.resx與MessageFile.zh-TW.resx。
將屬於en-us的訊息,放在MessageFile.en-US.resx中,將屬於zh-tw的訊息,放在MessageFile.zh-TW.resx中,如下圖所示:
在兩個Resource檔中,我們都加入了一個Name為"invalid"的資料。而我們期望,en-us讀出來的值就是"validate failed",而zh-tw讀出來的值就是"驗證失敗"。
接下來新增一個Message的class,有一個GetMessage(name)的方法,把name傳進來,就會取得語系所對應的訊息。
所以,就可以先來撰寫我們的測試程式:
///GetMessage 的測試
///</summary>
[TestMethod()]
public void GetMessageTest_測試en_us()
{
Message target = new Message(Message.Language.en_us);
string name = "invalid";
string expected = "validate failed";
string actual;
actual = target.GetMessage(name);
Assert.AreEqual(expected, actual);
}
/// <summary>
///GetMessage 的測試
///</summary>
[TestMethod()]
public void GetMessageTest_測試zh_tw()
{
Message target = new Message(Message.Language.zh_tw);
string name = "invalid";
string expected = "驗證失敗";
string actual;
actual = target.GetMessage(name);
Assert.AreEqual(expected, actual);
}
這兩個方法,就是當初始化Message時,選擇其文化語系,GetMessage方法中,傳入invalid參數,應該要取得對應的訊息。
接著來看Message的設計:
{
//可以根據需求來決定,要不要區分大小寫
private ResourceManager messageResourceManager = new ResourceManager("ResourceWithMultiLanguage.MessageFile", Assembly.GetExecutingAssembly());
public Message()
{
//可以根據情況來決定,是否要強制給一個預設的語系
}
public Message(Language language)
{
this.Format = language;
}
public enum Language
{
None = 0,
[Description("zh-tw")]
zh_tw,
[Description("en-us")]
en_us
}
private Language Format { get; set; }
public string GetMessage(string name)
{
//可以根據情況來決定,是否要用try/catch來處理,當name不存在於resource檔時,是否要攔錯,回傳空字串或null
var cultureInfo = this.Format == Language.None ? System.Threading.Thread.CurrentThread.CurrentCulture : new CultureInfo(this.Format.GetDescription());
var result = this.messageResourceManager.GetString(name, cultureInfo);
return result;
}
}
取得Enum的Description,定義在擴充方法中:
{
public static string GetDescription(this Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
var result = attributes.Length > 0 ? attributes[0].Description : value.ToString();
return result;
}
}
這邊只是很粗略的實作,透過ResourceManager來讀取MessageFile資源檔的內容。
程式碼說明
- 傳入Enum參數的建構式:用來讓使用者決定Message所使用的文化語系。使用Enum會失去對應的彈性,但好處是讓使用端可以明確的知道,目前Message所支援的語系有哪一些。
- 這邊也可以在Message無參數的建構式中,指定預設的語系為何。
- 建立一個ResourceManager,在ResourceManager的建構式,建立與MessageFile資源檔的關連。(可透過IgnoreCase屬性來決定,要不要區分大小寫)
- GetMessage()方法中,當Message有被指定語系,則讀取對應語系的資源檔。若沒有指定語系,則依照CurrentThread的CurrentCulture來決定CultureInfo。
- 當傳入的name,在對應語系的資源檔找不到name時,會throw MissingManifestResourceException。
針對第4點與第5點,補上test case:
///GetMessage 的測試
///</summary>
[TestMethod()]
public void GetMessageTest_預設語系為zh_tw()
{
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo("zh-tw");
Message target = new Message();
string name = "invalid";
string expected = "驗證失敗";
string actual;
actual = target.GetMessage(name);
Assert.AreEqual(expected, actual);
}
/// <summary>
///GetMessage 的測試
///</summary>
[TestMethod()]
[ExpectedException(typeof(MissingManifestResourceException))]
public void GetMessageTest_找不到對應的name()
{
Message target = new Message();
string name = "不存在的name";
string expected = string.Empty;
string actual;
actual = target.GetMessage(name);
Assert.AreEqual(expected, actual);
}
結論
當系統已經存活很久時,散落在系統各地的magic string,要克服i18n的問題時,真的又得花很多功夫跟時間來做整理。所以,請在您動手撰寫程式,hard-code字串時,再多想一下,即使只是簡單的抽成一個instance的member/field/property,就可以提昇系統的可讀性以及未來擴充的彈性。
當要回傳一個訊息時,多想一下,在這個layer, module回傳訊息,合理嗎?還是應該回傳一種狀態,再交給View去決定對應的訊息為何。
呈現訊息要修改時,會不會很難改?針對不同語系的訊息,是否要修改程式?
希望這個簡單的Sample,可以提供一些參考與幫助。
補充
之前忘了提醒,resx檔的屬性,要記得修改成內嵌資源。如圖所示:
Sampel Code
download: ResourceWithMultiLanguage.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/