[.NET]簡單實作多國語言的基底-使用Resource資源檔

  • 14931
  • 0
  • 2012-06-01

[.NET]簡單實作多國語言的基底-使用Resource資源檔

前言

在與同事討論程式碼的過程中,提到了magic string這個term。

magic string簡單的說,就是程式碼裡面出現hard-code的字串,而通常都是屬於『訊息』類的,這樣的字串既不好修改,也容易讓人不了解怎麼會突然間冒出這樣的字串出來。而訊息通常還要考慮到i18n與需要容易修改、抽換等情況…

這篇文章,就透過.NET的.resx資源檔,來解決magic string與多國語言的問題。

 

簡要說明

在.NET中,可以透過resource檔來放不同文化特性的資源檔,輕易地設計出具有彈性的程式碼。

  1. 透過ResourceManager來存取不同文化語系的資源檔。
  2. 程式碼邏輯與資源檔獨立開來。
  3. 程式碼中透過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中,如下圖所示:

image

image

在兩個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資源檔的內容。

 

程式碼說明

  1. 傳入Enum參數的建構式:用來讓使用者決定Message所使用的文化語系。使用Enum會失去對應的彈性,但好處是讓使用端可以明確的知道,目前Message所支援的語系有哪一些。
  2. 這邊也可以在Message無參數的建構式中,指定預設的語系為何。
  3. 建立一個ResourceManager,在ResourceManager的建構式,建立與MessageFile資源檔的關連。(可透過IgnoreCase屬性來決定,要不要區分大小寫)
  4. GetMessage()方法中,當Message有被指定語系,則讀取對應語系的資源檔。若沒有指定語系,則依照CurrentThread的CurrentCulture來決定CultureInfo。
  5. 當傳入的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檔的屬性,要記得修改成內嵌資源。如圖所示:

image

 

Sampel Code

download: ResourceWithMultiLanguage.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/