暫時匯總出了以下幾種方法
-
以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 參數可以從 MSDN的 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 作為轉換的中介。可以參閱 iconv 相關網站。
- 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 是GNU國際化與本地化(i18n)函數庫。它常被用於編寫多語言程序。
開發
程序源代碼需要進行修改以響應 GNU gettext 請求。多數編程語言均已通過字符封裝的方式實現了對其的支持。為了減少輸入量和代碼量,此功能通常以標記別名 _ 的形式使用,所以例如以下C語言代碼:
printf(gettext("My name is %s.\n"), my_name);
應當寫作:
printf(_("My name is %s.\n"), my_name);
gettext使用其中的字符串尋找對應的其他語言翻譯,若沒有可用翻譯則返回原始內容。
除C語言外, GNU gettext 還支持 C++, Objective-C,Pascal/Object Pascal,sh 腳本,bash 腳本,Python,GNU CLISP,Emacs Lisp,librep,GNU Smalltalk,Java,GNU awk,wxWidgets(通過 wxLocale類),YCP (YaST2語言),Tcl,Perl,PHP,Pike,Ruby以及R。用法均與在C語言上類似。
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 ""
翻譯者需要手工或使用類似 Poedit、gtranslator或Emacs等工具的相應模式編輯該文件。翻譯完成后,文件應為如下的樣子:
#: src/name.c:36
msgid "My name is %s.\n" msgstr "Je m'appelle %s.\n"
最后 .po 文件需要使用msgfmt編譯為.mo文件以用作發布。
運行
使用Unix類型操作系統的用戶只需設置環境變量中的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 是最新版本,GNU官網上可以直接下載,只是沒有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網站上有相應的工具軟件可以編輯與生成;
點擊這里下載Visual C++(MSVC)中可用的GNU gettext-0.18.3.2運行支持庫
基於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): 國際化(上)》: http://devbean.blog.51cto.com/448512/244689
《Qt學習之路(34): 國際化(下) 》: http://devbean.blog.51cto.com/448512/245063