gnu gettext と locale ja.


Gnu gettextをつかって時々うまく翻訳文字列が得られないことがありました。まず、gettextをinfoで読みました。php でgettextを使っていたので、phpのドキュメントにも目を通しました。余談ですが、php上のgettextは、c言語のapiのgettext libraryのwrapperになっています。なぜgettextが動作期待したとおりに処理されないのか、理解できていません。

gettextを調べるためのプログラム

gettext libraryを調べるためのプログラムのディレクトリ構成です。

bin
|-- i18n
|   |-- en
|   |   `-- LC_MESSAGES
|   |       `-- messages.mo
|   |-- en_US
|   |   `-- LC_MESSAGES
|   |       `-- messages.mo
|   |-- ja
|   |   `-- LC_MESSAGES
|   |       `-- messages.mo
|   `-- ja_JP
|       `-- LC_MESSAGES
|           `-- messages.mo
`-- i18n-msg

このソフトウェアはここから入手できます。

このプログラムはコンソールにメッセージを出すだけのプログラムです。
"-l ja_JP -c UTF-8“というコマンド引数(オプション)を指定して実行すると、日本語変換されたメッセージがコンソールに出力されます。次のようなオプションでは、翻訳されたメッセージを得ることができません。

  • -l ja
  • -l ja -c UTF-8
  • -l ja_JP

以下がgettextに関する呼び出しシーケンスです。

/* locale はコマンド引数 */
setloale(LC_MESSAGES, locale);

/* domain_dir は "i18n ディレクトリまでの絶対パス" */
bindtextdomain("messages", domain_dir);

/* codesetはコマンド引数 */
bind_textdomain_codeset("messages", codeset);

setlocaleが成功しない

上に書いたコマンド引数のいくつかでは、setlocaleが失敗します。

$> bin/i18n-msg -l ja
failed to bind text domain with ja

上にあるとおり、ロケールjaは存在しますが、setlocaleは失敗します。

文字化けする

以下のようなオプションでは、文字化けします。

$> bin/i18n-msg -l ja_JP
gettext????????????

用意したmessage.moのファイルはUTF-8です。そして、環境変数のLC_CTYPEは、en.UTF-8になっています。man 7 によると、LC_CTYPEが表示する文字列のために使用されると記載があります。

setlocaleが失敗する理由

ロケールに関するmanページを読みました。setlocaleを呼ぶ前にlocaleを用意しておく必要があることを知りました。私のlinux環境では、以下のlocaleが用意されていました。

$> locale -a
aa_DJ
aa_DJ.iso88591
aa_DJ.utf8en_US
...
en_US.iso88591
en_US.iso885915
en_US.utf8
en_ZA
...
ja_JP
ja_JP.eucjp
ja_JP.ujis
ja_JP.utf8
japanese
japanese.euc
...
zu_ZA
zu_ZA.iso88591
zu_ZA.utf8

‘ja’が上のリストにありません。これが、setlocaleが成功しない理由でした。

setlocale(“ja”)を成功させるために

setlocale(“ja”)を成功させるには、”ja” のロケールを用意しなければなりません。これは、”localedef”を実行することで、達成できます。ロケールはディレクトリ構造で、その配下のファイルは、ロケール関連の関数のバイナリデータになってます。デフォルトでは、ロケールデータはsystemディレクトリにあります。新しいカスタムロケールを作る場合などには、簡単な方法とはなっていないと感じました。そこで、新しい”ja”をログインユーザのディレクトリに用意して、環境変数’LOCPATH’を設定して、’ja’ロケールが得られるようにしました。

“ja”を私のlinux環境で用意するために、以下のコマンドを使いました。

$> localedef -i ja_JP -f UTF-8 locales/ja

以下はsetlocaleを成功させるための処理の一例です。

    char* locale_res;
    int result;
    locale_res = setlocale(LC_MESSAGES, locale); 
    if (!locale_res) {
        char* exec_dir;
        exec_dir = get_executable_dir();
        if (exec_dir) {
            size_t exec_dir_len;
            size_t locales_dir_len;
            size_t loc_path_buffer_size;
            const char* locales_dir = "locales";
            char* loc_path_buffer;
            char* saved_loc_path;
            locales_dir_len = strlen(locales_dir);
            exec_dir_len  = strlen(exec_dir);
            loc_path_buffer_size = exec_dir_len;
            loc_path_buffer_size += locales_dir_len;
            loc_path_buffer_size += 1;
            loc_path_buffer = (char*)malloc(loc_path_buffer_size);
            /* 設定済のlocpath環境変数 */
            saved_loc_path = getenv("LOCPATH");
            if (loc_path_buffer) {
                snprintf(loc_path_buffer, loc_path_buffer_size,
                   "%s%s", exec_dir, locales_dir);
                /* loc_path_buffer は"locales" ディレクトリ */
                /* "locales"ディレクトリは、実行ファイルがあるディレクトリ直下にある。*/    
                setenv("LOCPATH", loc_path_buffer, 1);
            }
            /* setlocaleは成功するでしょう */
            locale_res = setlocale(LC_MESSAGES, locale); 
            result = locale_res ? 0 : -1; 
            if (saved_loc_path) {
                /* LOCPATH 元に戻す */
                setenv("LOCPATH", saved_loc_path, 1);
            } 

            if (saved_loc_path) {
                free_str(saved_loc_path);
            }

            if (loc_path_buffer) {
                free_str(loc_path_buffer);
            }
        }
        if (exec_dir) {
            free_str(exec_dir);
        }    
    } else {
        result = 0;
    }
    return result;

gettextから文字化けしない文字列を取得する

gettextから文字化けしない文字列を取得するためには、setlocale(LC_CTYPE, <locale.codeset>)または、 bind_textdomain_codeset(domain, <codeset>)を呼ぶ必要があります。

setlocale(LC_CTYPE, <locale.codeset>) を呼ぶ

gettextのソースコードを読みました。戻りの文字列は、iconvによって変換されることが判りました。iconvは変換先のcodesetにメッセージdomainにと紐ついたものを使用します。もし紐ついたものがない場合は、nl_langinfo(CODESET)の戻り値を使用します。nl_langinfo(CODESET)は、setlocale(LC_TYPE, <locale.codeset>)によって設定されます。man 3 setlocaleによると、linux programの初期のlocaleは”C”です。環境変数は、異なる設定になっているかもしれません。locale関連の環境変数は、プログラムの初期の状態では影響しないのです。

こちらで確認した、locale関連の環境変数です。

LANG=en_US.UTF-8
LC_CTYPE=en_US.UTF-8
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_PAPER="en_US.UTF-8"
LC_NAME="en_US.UTF-8"
LC_ADDRESS="en_US.UTF-8"
LC_TELEPHONE="en_US.UTF-8"
LC_MEASUREMENT="en_US.UTF-8"
LC_IDENTIFICATION="en_US.UTF-8"
LC_ALL=

locale “C”では、 nl_langinfo(CODESET) は”ANSI_X3.4-1968″を返しました。これが、文字化けする理由でした。

bind_textdomain_codesetを呼ぶこと

bind_textdomain_codesetを呼ぶことは、直線的で判りやすい方法です。man bind_textdomain_codesetを読めば、理解できると思います。少し上で述べたとおり、この関数はあるドメインに、codesetを紐つけます。そして、iconvの出力パラメータとして用いられます。

最後に

gettextを使うにあたって、次のことを考慮に入れる必要あるでしょう。

  1. 使いたいlocaleが見つからない場合、localedefを使用してLOCPATH 環境変数を設定する。
  2. gettextを使用して文字化けしない文字列得るには、setlocale(LC_CTYPE, <locale.codeset>)または、 bind_textdomain_codeset(<domain>, <codeset>) を呼び出す。