君子善假于物:多语言开发介绍

 
谈到多语言开发,首先跃进我们脑海的 , 最简单,最直接的办法,就是在要显示语言相关字符的地方用个 If … Else 判断, 根据当前语言设置把相关字符串硬编码到程序中。其伪代码如下:
 
If language is English than
     Show English
Else if language is Chinese
     Show Chinese
 
这种方法简单,直接。但是遍布程序的 IF…Else 判断,不仅仅是让我们在每个判断处多写四五行代码。当我们面对几千,甚至几万个这样的判断得时候,随时可能漏掉其中的一部份,这会让我们你的程序看起来像这样:
 
Hello.
这是一个多语言程序的例子。 (此处本应是英文) .
It demonstrates how to deliver a multilingual program.
 
如果我们不想让自己写出来的多语言程序变成像上面一样多语混合的怪物的话,很有必要这他进行改进,最小化其中重复的 If … Else 判断。怎样才能避免这种重复的判断呢?
改进还是障眼法?
我见过一个支持中(繁体)英文双语 MFC 程序的实现。它把所有的多语言相关的字符串放到资源文件的 String Table 中。然后通过一个 MultilingualLoadString 函数根据当前语言设置加载相应的字符串。该函数实现伪代码如下:
 
void MultilingualLoadString(CString& str,UINT ID)
{
   if( g_language == 'English')
           str.LoadString(ID);             //Load English string
   else
           str.LoadString(++ID);           //Load Chinese string
}
 
 
            这个 MultilingualLoadString 函数的实现看起来, 嗯, 有点像,魔术。是的,魔术。在看到结果或注释之前,你不会想到它的作用是加载不同语言版本的字符串。一个简单的 ++ID 是怎样将相应的中英文字符串区分开来的?让我们来看看魔术背后的道具, MFC String Table.

           
                                                1 - String Table
 
String Table 1 - String Table )包含了三个字段, ID, Value, Caption. ID 其实是 Value 字段的一个别名 ( 编译时替换为相应 Value ) CString 类的 LoadString Value 的取值加载字符串。 MultilingualLoadString 的实现依赖于,加字符串到 String Table 的时候设置所有中文字符串的 Value 值比与其对应的英文字符串大 1 且只大 1 这样一个前提。
 
使用 方案 1 (我们把这一实现称为 方案 1 )后,添加新的语种看起来像是变得简单了。我们所要做的只有两件事:加一个 Else 分支和往 String Table 中添加新的字符串。但是,在我为这巧妙的实现举杯庆祝之前,再看看 String Table MultiLoadString 所依赖的大前提——各种语言向对应字符串的 Value 值必须是连续的整数。这意味着往 String Table 添加字符串的时候, 我们必须显式指定每个字符串的 Value 值。当我们需要添加第三种语言的支持的时候,这一前提依赖导致的结果是——我们必须重新设定 String Table 里所有字符串的 Value (如 1 - String Table )……
 
这让我想起了小时候玩的一个游戏。从 1 开始数数,看能数到多大而不出错。现在,我们大家也一起来玩玩这个游戏。在继续往下读这篇文章之前,停下来,开口数数: 1 2 3 4 5 ……然后记下你的结果(建议大家在后面的回复中列出自己的成绩,比比谁最厉害。如果你数到 500 还没出错,请马上联系我,我很愿意认识你)。
 
如果你刚才玩过这个游戏(如果没玩,请先玩这个游戏,之后再往下读)的话,相信你会有和我相同的看法——添加新语种的支持时,手动给 String Table 里的字符串编号出错的可能性比在源代码里硬编码字符串更高,需要更多的时间和成本来预防及测试。也就是说, 方案 1 并没有让事情变得简单,而是相反,让它更复杂了。怎样才能同时避免复杂的 If…Else 判断和手动字符串编号?怎样才能最小化在添加新语种是所需的改动?
从可以解决的小问题入手
暂且抛开手动字符串编码的复杂性不谈。所有的字符串放在同一个资源文件中,为资源文件的根新带来了很大的不便。为此,我们按字符串语言的不同将其分为两个资源文件。如 1 - String Table 可分为如下两个资源文件:

IDS_TEST             106                   Test
IDS_YEAR             108                   Y
IDS_MONTH                     110                   M
IDS_DAY               112                   D
IDS_YES                           114                   Yes
 
File 1   Resources.en.rc
 
IDS_TEST             107                   測試
IDS_YEAR             109                  
IDS_MONTH                     111                  
IDS_DAY               113                  
IDS_YES                           115                  
 
File 2   Resources.zh.rc

细心的读者可能已经注意到,拆分后,中文字符串的 ID 少了 _C” ,改为与相应的英文字符串 ID 相同。正是这一小小的改动,让另一个新的字符串加载方案变得可行。这个新的字符串加载方案是:根据当前语言设置从不同的文件加载,其实现伪代码 ( 本文主要介绍多语言开发的方案,不对其 MFC 实现进行展开 ) 如下:
 
String GetString(string, language)
{
If language is English
                 Load string from Resources.en.rc
Else
                 Load string from Resources.zh.rc
Return loaded string
}
 
方案 2
 
我们将这一实现称为 方案 2 方案 2 根据资源文件名的来区分不同语言版本的字符串,而不再是 方案 1 中的每个字符串对应的 Value 值,也就是说, 方案 1 中一直困扰我们的字符串排序问题也不复存在了。需要添加新的语种时, 方案 2 只需添加相应的资源文件同时在函数 GetString 里加一个 Else 分支即可。
 
            相对于 方案 1 来说, 方案 2 是个激动人心的改进。但 方案 2 仍有它的不足之处。第一,需要添加新的语种时,仍需修改 GetString 函数。修改意味着重新编译,重新测试,有出错的风险,需要更大的工作量。一个好的设计应该是对修改封闭,对扩展开放的。第二,它只支持字符串的多语言化。一个多语言的项目语言相关的不仅仅是字符串,还有音频,位图等资源。针对以上两点不足,我们提出了下面的方案 3.
更上一层楼
为了使 方案 2 具有更大的可扩展性,我们将其实现为一个独立的 Resource 类。其主要成员如下:

 
    class Resource
    {
        public Resource(string resFile)
        {
            this.resFile = resFile; //set the resources file
        }
 
        public string GetString(string str)
        {
            //load string from resFile
        }
 
        public object GetObject(string obj)
        {
            //load other resources (eg. Bitmap , audio ) from resFile
        }
 
        public string ResFile
        {
            get
            {
                return resFile;            //return current resource file
            }
            set
            {
                resFile = value;           //set the resource file at run time
            }
        }
 
        private string resFile;
}
 
方案 3
 
Resource 类的资源加载函数 GetString GetObject 根据成员变量 resFile 从不同的资源文件中加载。调用方只须简单的调用 Resource 类的成员函数 GetString 就可以加载特定语言的字符串(加载位图等其他资源可调用 GetObject 函数)。 同时, resFile 的读写器提供了运行时更改资源文件的接口,以支持动态语言切换。 Resource 类在需要更新资源文件或添加新的语种时支持时无需任何代码改动。
君子善假于物
多语言开发并不是一个新鲜的话题,相反,它有很多现成的工具支持。本文主要介绍多语言开发的思路,但我并不提倡重新发明轮子。利用现有的工具,能有效地加快我们的开发速度,减少工作量,时间,和成本。
.NET Framework 提供了类似 Resource 类的 ResourceManager 类。 ResourceManager 类可以查找特定区域(语言)的资源。 通过其成员函数 GetString GetObject 访问特定区域(语言)的资源。默认情况下,这些方法返回当前区域(语言)(由 Thread.CurrentThread.CurrentUICulture Thread.CurrentThread.CurrentCulture 指定, 如果没有显式指定,则为当前操作系统设置 的资源。下面提供一个使用 ResourceManager 的例子,具体请参阅 MSDN
 
假设: 已经在项目中添加资源文件 MultilingualString.resx MultilingualString.zh-CN.resx. “Hello” 为资源文件中的字符串。
 
using System;
using System.Collections.Generic;
using System.Text;
using System.Resources;
using System.Reflection;
using System.Globalization;
using System.Threading;
 
namespace MultilingualHello
{
    class Program
    {
        static void Main(string[] args)
        {
            ResourceManager rm = new ResourceManager("MultilingualHello.MultilingualString", Assembly.GetExecutingAssembly());
            CultureInfo ci = new CultureInfo("zh-CN");
            Thread.CurrentThread.CurrentUICulture = ci;
            string str = rm.GetString("Hello");
            Console.WriteLine(str);
        }
    }
}
 
Sample 1 ResourceManager Sample
           
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值