2013年9月30日月曜日

Boost.Localeを使ってアプリケーションの国際化

はじめに

今までプログラムを作るときは、文字コードの問題があるのでなるべく日本語の表示を 避け、なんちゃって英語でメッセージを出力してきましたが、もうそろそろそういうことはやめようと思います。 ただ日本語でメッセージを書いても面白くないので、ここは一歩進んで多言語に対応できるようにしようとおもいます。 いつの日か私の作ったハローワールドが世界に必要とされる日が来るかもしれません。

ということで今回は Boost.Locale を使ってC++のアプリケーションを国際化します。 C言語のアプリケーションを国際化する場合、GNUのgettextがありますが、 それだけのためにgettextをプログラムに組み込むよりは、Boostの他の機能の使う ついでに国際化する方が楽なのではないでしょうか。

とは言うものの私はアプリケーションの国際化については、まったく無知なので、間違いが あるかもしれませんが、なにとぞよろしくお願い致します。

Boost.Locale の準備

Boost.Locale を使用すると、C++のアプリケーションの出力を日本語や英語など環境によって、切り替えることができます。 アプリケーションの国際化にはGNUのgettextがありますが、 Boost.Locale はそれと 似た仕組みとなっています。というかほとんど同じで、メッセージの抽出・翻訳にはgettext付属のツール を使うので、gettextが必要になります。プログラム中に英語でメッセージを書きそれを ツールで抜き出して、翻訳し、最後にバイナリファイルにします。

gettextはWindows環境ではCygwinを使えば簡単に手に入るので、今回はこれを使用することに します。Cygwinでgettext-develをインストールしておいてください。

余談ですが、cygwinでは apt-cyg というツールを使うとコマンドラインから以下のように簡単にソフトウェアを入れることができます。 しかし、最近使えなくなってしまったので、このサイト にあるものを使わせてもらっています。:

apt-cyg install gettext-devel

あと、boostを用意しなければなりません。boostの準備は私が 以前に書いたエントリ と同じですので、今回は割愛します。boostのコンパイルまで行ってください。

それと最後にcmakeをインストールしてください。

CMakeFileの作成

別にCMakeを使用しなくてもいいのですが、楽なので今回も使用します。 local_testというフォルダを作成したら中にCMakeFile.txtという名前で以下のようなテキスト ファイルを作成します。 今回はBoostのsystemとlocaleコンポーネントが必要となります。

cmake_minimum_required(VERSION 2.8)

set(TARGET_NAME local_test)
project(${TARGET_NAME})

## Boost

set(ENV{Boost_DIR} "C:/_Lib/boost/boost_1_53_0")
set(Boost_ADDITIONAL_VERSIONS "1.53" "1.53.0")

set(Boost_USE_STATIC_LIBS ON)
set(Boost_USE_MULTITHREADED ON)
set(Boost_USE_STATIC_RUNTIME OFF)

find_package(Boost 1.53.0 COMPONENTS system locale)
if(NOT Boost_FOUND)
  message(SEND_ERROR "Boost is not found!!")
endif()

## BUILD

file(GLOB TARGET_SOURCES *(.c|.cpp|.h))
add_executable(${TARGET_NAME} WIN32 ${TARGET_SOURCES} ${GENERATED_SOURCES})
set_target_properties(${TARGET_NAME} PROPERTIES DEBUG_POSTFIX _d)

include_directories(${Boost_INCLUDE_DIR})
target_link_libraries(${TARGET_NAME} ${Boost_LIBRARIES})

プログラムの作成

さて、ここまで来たらやっとプログラムを組むことができます。先ほどCMakeFile.txtを作成した フォルダにlocale_test.cppという名前で以下のようなプログラムを作成します。

  #pragma comment(linker, "/SUBSYSTEM:CONSOLE")
  #include <boost/locale.hpp>
  int main()
  {
      boost::locale::generator gen;
      gen.add_messages_path("./locale");
      gen.add_messages_domain("locale_test");

      std::locale::global(gen(""));
      std::cout.imbue(std::locale());

      std::cout << boost::locale::translate("Hello World") << std::endl;
  }

さて、これでプログラムは完成です。少しだけ説明しますと、add_messages_pathでカレントディレクトリの local以下からメッセージファイルを探すように指定しています。でもカレントディレクトリが別の場所にある 場合、メッセージを探せないのであまりいい方法ではありません。Linuxなどであれば/usr/share/locale にまとめてメッセージがあるので、そこを指定するといいと思います。

次のadd_messages_domainはメッセージファイルの名前です。この場合locale_test.moというファイルが メッセージファイルとなります。

その下2行はよくわからないので、飛ばしまして、最後はHellow Worldというメッセージを翻訳して出力するよう にしています。最後にreturnがないのはただ忘れただけです。

それではビルドしましょう。今回は全部コマンドラインでやります。 GUIで作業したい場合は、 以前のエントリー を参照してください。

環境はWindows 8 64bit版にVisual Studio 2010が入っています。32bit版のバイナリを作ることにします。 始めにVisual Studioの構築環境の設定とcmakeのパスを通しています。 その後、locale_test_buildというフォルダを作ってそこでビルドしています。:

> cd locale_test
> "%ProgramFiles(x86)%\Microsoft Visual Studio 10.0\VC\vcvarsall.bat"
> path "C:\Program Files (x86)\CMake 2.8\bin";%PATH%
> md ..\locale_test_build
> cd ..\locale_test_build
> cmake ..\locale_test -G "Visual Studio 10"
> MSBuild locale_test.vcxproj

なお、CMakeFile.txtを後から変更した場合は、locale_test_buildディレクトリで「cmake .」 と打つだけで更新できます。ピリオド「.」を入れ忘れないでください。

では実行してみましょう。:

> cd Debug
> locale_test_d
Hello World

何の変哲もないハローワールドが動きました。

翻訳

ではこのHello Worldを翻訳したいと思います。cygwinを起動するのが面倒なので パスを通して使用することにします。まずはxgettextコマンドでソースコード中のメッセージを 抜き出します。:

cd ..\..\locale_test
path c:\cygwin\bin;%PATH%
xgettext.exe --c++ --from-code=UTF-8 --keyword=translate locale_test.cpp -o locale_test.pot

-keywardに指定した文字をxgettextが探してきて、POTファイルに入れてくれます。 一応これでもできますが、boostのtranslateコマンドには、引数が複数あることがあり、それを考慮する場合は、 以下のような途方もなく長いコマンドを使用するように

書かれています。

xgettext --keyword=translate:1,1t --keyword=translate:1c,2,2t       \
         --keyword=translate:1,2,3t --keyword=translate:1c,2,3,4t   \
         --keyword=gettext:1 --keyword=pgettext:1c,2                \
         --keyword=ngettext:1,2 --keyword=npgettext:1c,2,3          \
         source_file_1.cpp ... source_file_N.cpp

hoge.potファイルが作成され、中には英語のメッセージが書かれています。 これを翻訳した日本語POファイルを作らなければなりません。 POファイルはmsginitコマンドを利用してPOTファイルから以下のように作成します。:

msginit -l ja -i locale_test.pot

ja.poファイルが作成されるので、中身を翻訳します。ja.poをテキストエディッタで 開くとmsgidと書いた後ろに英語のメッセージが書かれていますので、すぐ下のmsgstrに 翻訳したものを書き込みます。:

#: locale_test.cpp:12
msgid "Hello World"
msgstr "ハローワールド"

なお、poファイルのエンコードですが、UTF-8のBOMなしで保存するようにしてください。 また、上部にあるヘッダを少し書き換えます。:

"Project-Id-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"

最後に翻訳したja.poファイルをコンパイルしてMOファイルを作ります。 それにはmsgfmtコマンドを使用します。:

msgfmt ja.po -o locale_test.mo

最後にlocale_test.moファイルが作成されるました。 MOファイルの置き場所ですが、プログラムでカレントディレクトリの下のlocaleディレクトリを指定したので、 プログラムと同じディレクトリにlocaleフォルダを作成します。 そしてその下に、jaフォルダ、その下にLC_MESSAGESフォルダを作成し、 LC_MESSAGEフォルダの中にlocale_test.moに置きます。結構階層が深いです。

ではいよいよ翻訳したハローワールドを表示する時がきました。 では実行しましょう:

> locale_test_d
繝上Ο繝シ繝ッ繝シ繝ォ繝

見事文字化けしてくれました。おそらくプログラムはUTF-8で出力しているのですが、 コマンドプロンプトはShift-JISで解釈しているためと思います。試しにUTF-8 のCygwinシェルを使うと正しく表示されます。

chcp 65001でコマンドプロンプトのコードページを切り替えると今度はフォントが 英語になるため、日本語を表示できませんでした。ja.poファイルをShift-JISで 作ってみたり色々試してみたのですが、うまくいきませんでした。

ほとんど諦めかけていたのですが、たまたまというわけではありませんが、 以下のコマンドを打ったらうまくいきました。:

> set LC_CTYPE=ja_JP.SJIS
> locale_test_d
ハローワールド

boost.localeがコード変換してくれているのでしょうか。 でも変換ルーチンはどこにあるのでしょう。 作成されたたった1MBのバイナリの中にあるとは思えません。 よくわからないですが、うまくいったので良しとします。

なお、プログラム中のstd::locale::globalの部分を以下のように書き換えても うまくいきます。:

    std::locale::global(gen(“ja_JP.SJIS”));

その他

POTファイルから日本語POファイルの作成ですが、poEditというソフトを使うと とっても楽になります。おまけにMOファイルまで作ってくれます。至れり尽くせりです。 でも、POTファイルの作成は自分でやらなければなりません。

毎回boost::locale::translateと入れるのはちょっと長すぎです。 そういうときは落ち着いて、以下のようなマクロを定義するとGetTextを使った時と同じように短く書けます。:

#define _(S)    (boost::locale::translate(S))
std::cout << _("Hello World") << std::endl;

その時はxgettextも以下のようにしましょう。:

xgettext.exe --c++ --from-code=UTF-8 --keyword=_ locale_test.cpp -o locale_test.pot

中に変数の値を入れたい場合、こうしましょう。とても長いので、マクロをつくなり、ネームスペースをusingするなり した方がいいでしょう。:

boost::locale::format(boost::locale::translate("Hello world {1}")) % num

次いでにstd::stringにするならこうです。:

(boost::locale::format(boost::locale::translate("Hello world {1}")) % num).str()

POTファイルを後から更新したら、msgmergeコマンドでPOファイルを更新することができます。

というわけで長くなりましたが終わります。

0 件のコメント:

コメントを投稿