暂时汇总出了以下几种方法
-
以Unicode为核心
-
采用 GNU gettext
-
基于Qt的多语言开发工具:Qt Linguist
以Unicode为核心
参考:http://www.ibm.com/developerworks/cn/linux/l-cn-ccppglb/
多国语言的存在,使程序员在编码处理上花费了大量时间和精力;然而各种各样的乱码问题,如 XML 格式错误、文本显示异常、解析器异常等依然层出不穷。特别的,相对于 JAVA 语言,C/C++ 在处理编码问题上有更大的困难。本文避免纠缠不同编码格式的具体异同,以 Unicode 为核心,以简体中文为例,从工程应用角度分析编码问题存在的原因,不仅提出 C/C++ 标准库编程的解决方案,更结合项目经验,总结出处理多国语言编码问题的一般思路。
问题的提出
多国语言的存在、不同语言操作系统的存在,使得针对多语言的设计颇费周章,在编码上所付出的工作量也是可观的。所谓编码的问题,归结起来,就是二进制的编码以何种编码格式进行解析的问题。特别是在硬盘文件和内存数据的相互转化、即读写过程中,如果采用了错误的编码格式,就会造成乱码。JAVA 语言在字符串、编码等处理方面给了程序员更为直接、方便的接口,习惯使用 JAVA 做编码的程序员,在使用 C/C++ 进行文本编码相关的操作时,常会感到困惑。本文的目的在于以常用的 Unicode(UCS-2)、GB2312、UTF8 三种编码为例,分析不同编码在实用中的关系,特别是 C/C++ 中,怎样处理各种编码的问题。
编码处理常见的问题
- 1. 将内存中编码 A 的字符串以编码 B 格式处理成字节流写入文件
- 2. 将原本以 A 编码组成的文件以字节流形式读入内存、并以编码 B 解析为字符串。
第一种情况,可能造成数据的变化、失真。
如果使用 JAVA 语言,发生这种错误的情况稍少一些,因为在 JAVA 中没有 wstring 这种概念,在内存中的 String,使用的编码都是 Unicode,其中的转换对于程序员来讲是透明的。只要使用输入 / 输出方法时注意字节流的字符集选择即可。例如,编码为中文 GB2312 的“标准”字符串被读入内存后转存为 UTF8 的过程:图 1. 文件转换编码的 JAVA 处理方式
但 C/C++ 编程,由于通常使用 char、string 类型的时候比较多,特别是进行文件读写,基本都是操作 char* 类型的数据。并且也没有像 JAVA 中 getByte(String charsetname) 这种函数,不能直接根据字符集重新编码得到字符串的 byte 数组。这时候,我们使用的 string 其实就一般不是 Unicode,而是符合某种编码表的。这使得我们往往困惑于 string 的编码问题。假设有 utf8 的字符串“一”(E4 B8 80),而我们错误的认为它是符合 gb2312(编码 A)的,并将其转换为 utf8(编码 B),这种转换结果是破坏性的,错误的输出将永远无法正确识别。
依然以“标准”为例,这是一个正确的转换:图 2. 文件转换编码的 C/C++ 处理方式
第二种情况,则是更常见到的。例如:浏览器浏览网页时的发生的乱码问题;在写 XML 文件时,指定了 < ?xml version="1.0" encoding="utf-8" ?> 然而文件中却包含 GB2312 的字符串——这样经常会导致 XML 文件 bad formatted,而使得解析器出错。这种情况下,其实数据都是正确的,只要浏览器选择正确的编码,将 XML 文件中的 GB2312 转换为 UTF8 或者修改 encoding,就可以解决问题。需要注意的是,ASCII 码的字符,即单字节字符,一般不受编码变动影响,在所有编码表中的值是一样的;需要小心处理的是多字节字符,例如中文语言。
编码转换方法
一般的编码转换,直接做映射的不太可能,需要比较多的工作量,大多情况下还是选择 Unicode 作为转换的中介。
使用库函数
如前文所说,JAVA 的 String 对象是以 Unicode 编码存在的,所以 JAVA 程序员主要关心的是读入时判断字节流的编码,从而确保可以正确的转化为 Unicode 编码;相比之下,C/C++ 将外部文件读出的数据存为字符数组、或者是 string 类型;而 wstring 才是符合 Unicode 编码的双字节数组。一般常用的方法是 C 标准库的 wcstombs、mbstowcs 函数,和 windows API 的 MultiByteToWideChar 与 WideCharToMultiByte 函数来完成向 Unicode 的转入和转出。
这里以 MBs2WCs 函数的实现说明 GB2312 向 Unicode 的转换的主要过程:
清单 1. 多字节字符串向宽字节字符串转换
wchar_t * MBs2WCs(const char* pszSrc){ wchar_t* pwcs = NULL; intsize = 0; #ifdefined(_linux_) setlocale(LC_ALL, "zh_CN.GB2312"); size = mbstowcs(NULL,pszSrc,0); pwcs = new wchar_t[size+1]; size = mbstowcs(pwcs, pszSrc, size+1); pwcs[size] = 0; #else size = MultiByteToWideChar(20936, 0, pszSrc, -1, 0, 0); if(size <= 0) returnNULL; pwcs = new wchar_t[size]; MultiByteToWideChar(20936, 0, pszSrc, -1, pwcs, size); #endif returnpwcs; }
相应的,WCs2MBs 可以将宽字符串转化为字节流。
清单 2. 宽字节字符串向多字节字符串转换
char* WCs2MBs(const wchar_t * wcharStr){ char* str = NULL; intsize = 0; #ifdefined(_linux_) setlocale(LC_ALL, "zh_CN.UTF8"); size = wcstombs( NULL, wcharStr, 0); str = new char[size + 1]; wcstombs( str, wcharStr, size); str[size] = '\0'; #else size = WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, NULL, NULL, NULL, NULL ); str = new char[size]; WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, str, size, NULL, NULL ); #endif returnstr; }
Linux 的 setlocale 的具体使用可以参阅有 C/C++ 文档,它关系到文字、货币单位、时间等很多格式问题。Windows 相关的代码中 20936 和宏定义 CP_UTF8 是 GB2312 编码对应的的 Code Page[ 类似的 Code Page 参数可以从 的 Encoding Class 有关信息中获得 ]。
这里需要特别指出的是 setlocale 的第二个参数,Linux 和 Windows 是不同的:
- 1. 笔者在 Eclipse CDT + MinGW 下使用 [country].[charset](如 zh_CN.gb2312 或 zh_CN.UTF8)的格式并不能通过编码转换测试,但可以使用 Code Page,即可以写成 setlocale(LC_ALL, ".20936") 这样的代码。这说明,这个参数与编译器无关,而与系统定义有关,而不同操作系统对于已安装字符集的定义是不同的。
- 2. Linux 系统下可以参见 /usr/lib/locale/ 路径,系统所支持的 locale 都在这里。转换成 UTF8 时,并不需要 [country] 部分一定是 zh_CN,en_US.UTF8 也可以正常转换。
另外,标准 C 和 Win32 API 函数返回值是不同的,标准 C 返回的 wchar_t 数组或者是 char 数组都没有字符串结束符,需要手动赋值,所以 Linux 部分的代码要有区别对待。
最后,还要注意应当在调用这两个函数后释放分配的空间。如果将 MBs2WCs 和 WCs2MBs 的返回值分别转化为 wstring 和 string,就可以在它们函数体内做 delete,这里为了代码简明,故而省略,但请读者别忘记。
第三方库
目前的第三方工具已经比较完善,这里介绍两个,本文侧重点不在此,不对其做太多探讨。
- Linux 上存在第三方的 iconv 项目,使用也较为简单,其实质也是以 Unicode 作为转换的中介。可以参阅
- ICU 是一个很完善的国际化工具。其中的 Code Page Conversion 功能也可以支持文本数据从任何字符集向 Unicode 的双向转换。可以访问其
实验测试
在代码中调用“编码转换方法”一节里提到的函数,将 gb2312 编码的字符串转换为 UTF8 编码,分析其编码转换的行为:
在英文 Linux 环境下,执行下列命令:
export LC_ALL=zh_CN.gb2312
然后编译并执行以下程序(其中汉字都是在 gb2312 环境中写入源文件)
L1: wstring ws = L"一"; L2: string s_gb2312 = "一"; L3: wchar_t * wcs = MBs2WChar(s_gb2312.c_str()); L4: char* cs = WChar2MBs(wcs);
查看输出:
- L1 - 1 wide char: 0x04bb
- L2 - 2 bytes:0xd2,0xbb,即 gb2312 编码 0xD2BB
- L3 - 返回的 wchar_t 数组内容为 0x4E00,也就是 Unicode 编码
- L4 - 将 Unicode 再度转换为 UTF8 编码,输出的字符长度为 3,即 0xE4,oxB8,0x80
在 L1 行,执行结果显示编码为一个 0x04bb,其实这是一个转换错误,如果使用其他汉字,如“哈”,编译都将无法通过。也就是说 Linux 环境下,直接声明中文宽字符串是不正确的,编译器不能够正确转换。
而在中文 windows 下使用相同测试代码,则会在 L1 处出现区别,ws 中的 wchar_t 元素十六进制值是 0x4e00,这是汉字“一”的 Unicode 编码。
处理编码问题的经验总结
首先,这里先简单说明一下 Unicode 和 UTF8 的关系:Unicode 的实现方式和它的编码方式并不相同,UTF8 就是其实现之一。比方使用 UltraEdit 打开 UTF8 编码的中文文件,使用 16 进制查看,可以发现看到的中文对应部分应当是 Unicode 编码,每个中文字长度 2 字节—— UltraEdit 在这里已经做了转化;如果直接查看其二进制文件,可以发现是 3 字节。但两者的差别仅在于 Unicode 向 UTF8 做了数学上的转化。(更多关于 Unicode 和 UTF8 的概念,可以参见 )
其次,关于第三方库的选择,应当综合考虑项目的需求。一般的文本字符转换,系统的库函数已经可以满足需求,实现也很简单;如果需要针对不同地区的语言、文字、习惯进行编程,需要更为丰富的功能,当然选择成熟的第三方工具可以事半功倍。
最后,从逻辑上保持字符串的编码正确,需要注意几条一般规律:
- 编码选择:多国语言环境的编程,以使用 UTF 编码为原则,减少字符集转换。
- string 并不包含编码信息,但是编码确定了 string 的二进制内容。
- 读写一致:读入时使用的字符集要与写出时使用的一致。如果不需要改变字符串内容,仅仅是将字符串读入、再写出,建议不要调整任何字符集——即使程序使用的系统默认字符集 A 与文件的实际编码 B 不符合,写出的字符串依然会是正确的 B 编码。
- 读入已知:对于必须处理、解析或显示的字符串,从文件读入时必须知道它的编码,避免处理字符串的代码简单使用系统默认字符集;即便对于程序从系统中收集到的内存字符串,也应知道其符合的编码格式——一般为系统默认字符集。
- 避免直接使用 Unicode:这里是说将非 ASCII 编码的 16 进制或者 10 进制数值用 &# 与 ; 包含起来的使用方式,例如将中文“一”写成“e00;”。这种方法的实质是 Unicode 编码直接写入文件。这不仅会降低代码的通用性、输出文件的可读性,处理起来也很困难。比如法文字符在其他字符集中是大于 80H 的单字节字符,程序同时要支持中文的时候,很有可能会将多字节的中文字符错误割裂。
- 避免陷入直接的字符集编程:国际化、本地化的工具已经比较成熟,非纯粹做编码转换的程序员没有必要自己去处理不同编码表的映射转换问题。
- Unicode/UTF8 并不能解决一切乱码问题:Unicode 可以说是将世界语言统一起来的一套编码。但是这并不意味着在一个系统中可以正常显示的按照 UTF8 编码的文件,在另一个系统中也可以正常显示。例如,在中文的 UTF8 编码或者 Unicode 编码在没有东亚语言包支持的法文系统中,依然是不可识别的乱码——尽管 UTF8、Unicode 它们都支持。
采用 GNU gettext
参考:http://zh.wikipedia.org/wiki/Gettext
gettext 是(i18n)函数库。它常被用于编写多语言程序。
开发
程序源代码需要进行修改以响应 GNU gettext 请求。多数均已通过字符封装的方式实现了对其的支持。为了减少输入量和代码量,此功能通常以标记 _ 的形式使用,所以例如以下代码:
printf(gettext("My name is %s.\n"), my_name);
应当写作:
printf(_("My name is %s.\n"), my_name);
gettext使用其中的字符串寻找对应的其他语言翻译,若没有可用翻译则返回原始内容。
除外, GNU gettext 还支持 , ,/, 脚本, 脚本,,GNU ,,librep,GNU ,,GNU ,(通过 wxLocale类),YCP (语言),,,,,以及。用法均与在上类似。
xgettext程序从源代码生成 .pot 文件,作为源代码中需翻译内容的模板。一个典型的 .pot 文件条目应当是这样的:
#: src/name.c:36 msgid "My name is %s.\n" msgstr ""
被直接放置在字符串前,用于帮助翻译者理解待翻译内容:
/// TRANSLATORS: Please leave %s as it is, because it is needed by the program. /// Thank you for contributing to this project. printf(_("My name is %s.\n"), my_name);
本例中的注释是以 /// 开头的,其作用是用于 xgettext 程序生成 .pot 模板文件。
xgettext --add-comments=///
在 .pot文件中的注释应为以下形式:
#. TRANSLATORS: Please leave %s as it is, because it is needed by the program. #. Thank you for contributing to this project. #: src/name.c:36 msgid "My name is %s.\n" msgstr ""
翻译
翻译者需要工作的对象是 .po文件,它是由msginit程序从 .pot 模板文件生成的。例如使用msginit初始化法语翻译文件时,我们运行以下命令:
msginit --locale=fr --input=name.pot
这将会使用指定的 name.pot 在当前目录创建一个 fr.po,其中的一个条目应该是以下形式的:
#: src/name.c:36 msgid "My name is %s.\n" msgstr ""
翻译者需要手工或使用类似 、或等工具的相应模式编辑该文件。翻译完成后,文件应为如下的样子:
#: src/name.c:36 msgid "My name is %s.\n" msgstr "Je m'appelle %s.\n"
最后 .po 文件需要使用msgfmt编译为.mo文件以用作发布。
运行
使用类型操作系统的用户只需设置中的LC_MESSAGES
,程序将自动从相应的.mo
文件中读取语言信息。
补充:最新版 gettext-0.18.3.2可在MSVC中实现多语言
参考:http://www.aslike.net/showart.asp?id=154
“通常,程序及其文档信息都是用英语语言写的,程序运行时同用户交互的信息也是英语。这是一个事实,不仅仅GNU的软件是这样,其他大部分私有软件或自由软件也是这样。一方面,对于来自所有国家的开发者、维护者和用户来说,相互沟通中使用一种通用的语言非常的方便。另一方面,相对于母语来说大多数人并不适应使用英语,而且他们的日常工作都是尽可能的使用他们自己的母语。多数人都会喜欢他们的计算机屏幕显示的英语更少,显示的母语更多。"
" GNU 的 'gettext' 是 GNU翻译项目的一个重要步骤,我们依赖于它 作很多其他的步骤。这个软件包给程序员、翻译者,或者用户提供了一套集成工具和文档。详细地说,GNU gettext 提供了一套工具, 能让其他 GNU 软件创建多语言信息。..."
gettext的工作流程是这样的:比如我们写一个Visual C++(MSVC)程序,通常printf等输出信息都是English的。如果我们在程序中加入gettext支持,在需要交互的字符串上用gettext函数,程序运行是就可以先调用gettext函数获取当前语言的字符串,替换当前的字符串了。注意是运行时替换。
GNU gettext-0.18.3.2 是最新版本,上可以直接下载,只是没有Visual C++(MSVC)可用的运行支持库,只能自己动手编译了,编译好的运行支持库,。
在Visual C++(MSVC)中使用GNU gettext实现多语言时,可以编写翻译函数来实现界面与菜单字符串的自动替换,程序中的字符串只能一个个手工替换了,这样使用起来,就跟在Delphi与C++Builder中使用GNU gettext差不多方便快捷了。
简单使用的例子
一个简单的例子,
#include <stdio.h>
#include <libgnuintl.h>/*使用gettext通常使用类似下面的一个带函数的宏定义
*你完全可以不用,直接使用 gettext(字符串)*/#define _(S) gettext(S)/*PACKAGE是获取语言字符串的文件名字(运行时输入的命令)*/
#define PACKAGE "default"int main(int argc, char **argv)
{ /* 下面三个参数都是使用gettext时候需要使用的 * setlocale * bindtextdomain * textdomain*/ setlocale(LC_ALL,""); bindtextdomain(PACKAGE, "locale"); textdomain(PACKAGE);printf(_("Hello,GetText!\n"));
return 0;}其中语言字符串文件的结构: .\locale\语言名称\LC_MESSAGES\default.mo,如简体中文:.\locale\ZH_CN\LC_MESSAGES\default.mo
mo文件是编译后的语言字符串文件,GNU网站上有相应的工具软件可以编辑与生成;
基于Qt的多语言开发工具:Qt Linguist
参考:http://www.oschina.net/p/qt+linguist
http://www.oschina.net/question/54100_146029
http://www.oschina.net/question/54100_146030
http://devbean.blog.51cto.com/448512/244689
http://devbean.blog.51cto.com/448512/245063
Qt Linguist 是一个用来给 Qt 编写的应用程序增加多语言支持的工具。
QT-Linguist工具主要用在项目的多语言翻译处理过程中,所有先简单介绍一下整个多语言处理过程,最后介绍Linguist的用法。
(一)QT项目实现多语言,必须做两件事:
1)确保每一个用户可见的字符串都使用了tr()函数。
2)在应用程序启动的时候,使用QTranslator载入一个翻译文件(.qm)。 tr() 的用法: 1 | caseCheckBox = new QCheckBox(tr( "Match &case" )); |
在main()函数里载入翻译文件:
1 2 3 4 5 6 7 8 9 | int main( int argc, char *argv[]) { QApplication app(argc, argv); //翻译程序 QTranslator translator; translator.load( "spreadsheet_cn.qm" ); app.installTranslator(&translator); …… } |
注意:翻译文件加载的位置必须在界面实例化之前完成。
(二)生成.qm翻译文件
1、 在该应用程序的.pro文件文件中添加TRANSLATIONS项,可分别对应于不同的语言,如:spreadsheet_cn.ts, 对应中文,名字可以自己定义,后缀名.ts不可变动。<.ts是可读的翻译文件,使用简单的XML格式;而.qm是经过.ts转换而成的二进制机器 语言>
2、翻译文件。分三步来完成:
1)运行lupdate, 从应用程序的源代码中提取所有用户可见的字符串。 2)使用Qt Linguist 翻译该应用程序。 3)运行lrelease,生成二进制的.qm 文件。 以上三步均需用到QT自带的命令行控制台,启动方法:开始--->所有程序--->Qt by Nokia v4.6.3 (OpenSource)--->Qt 4.6.3 Command Prompt 启动命令行后,对应输入如下命令: 1)lupdate –verbose spreadsheet.pro //生成相应的.ts 文件 2)linguist //启动Linguist语言翻译工具,可以翻译相应可见字符串3)lrelease –verbose spreadsheet.pro //将翻译好的文件生成.qm文件
(三)Linguist 语言工具的使用
1)启动:命令行或者开始菜单均可
2)打开:工具界面中的File--->Open,可以打开所需的 .ts 文件 3)翻译:界面中部的翻译栏,两行:第一行:Source Text 第二行:… Translation, 在地二行进行相应的翻译即可,翻译完一条之后点击“确定下一个”按钮。 4)发布:点击File--->Release, 生成 .qm 文件。(与命令行的效果一样)(四)Linguist 语言工具使用方法建议
1、在代码中所有需要使用中文的地方都用一段英文暂时代替,并用tr()函数做标记。
2、使用Qt Linguist对所有被tr()函数标记的字符串进行翻译,并发布翻译包。
3、在程序中加载翻译包。
详细做法,可以见devbean大神的博客:
《Qt学习之路(33): 国际化(上)》:
《Qt学习之路(34): 国际化(下) 》: