シェルスクリプト(bash)のメッセージを国際化する 2013年版

nogajun
nogajun

_

シェルスクリプトのメッセージを国際化するといえば「bash スクリプト内メッセージの国際(カタログ)化」に書かれている方法が有名ですが、この方法はセキュリティ上の問題(シフトJISや中国語のエンコーディングではダブルクォーテーションを無効にできてしまうなど。くわしくは「bash - GNU `gettext' utilities」を参照)から現在、非推奨になっています。

現在推奨されているメッセージの国際化は、手順自体が大幅に変わったワケではありませんが、全体を通してまとまった文書は日本語ではないようなので、まとめておきます。

参考

以前との違い

大きな違いとしては、国際化したいメッセージの部分を「$"Msgid"」と書いていたものが、「"$(eval_gettext "Msgid")"」という書き方になりました。それにともない、事前にラッパーのgettext.shを読み込ませる必要があります。

シェルスクリプトのメッセージを国際化する

ここからは、シェルスクリプトのメッセージを実際に国際化対応していきます。

サンプルと実行例

サンプルとしてHello Worldと表示するシェルスクリプトを使いました。

#!/usr/bin/env bash
# hello.sh -- say hello script
echo hello world

実行例

$ ./hello.sh
hello world

メッセージのカタログ化事前準備

メッセージをカタログ化を準備するため、ラッパーのgettext.shを読み込ませます。 読み込ませる方法は「. gettext.sh」(一番前にドットがあります。)という行を追加します。 追加する行は、先頭の部分、シェバンの下あたりに書くとよいでしょう。

#!/usr/bin/env bash
# hello.sh -- say hello script
. gettext.sh
echo hello world

メッセージをカタログ化

国際化したいメッセージをカタログ化します。 まず、メッセージの部分をダブルクォーテーションで囲み、その前に「eval_gettext」と書きます。

#!/usr/bin/env bash
# hello.sh -- say hello script
. gettext.sh
echo eval_gettext "hello world"

eval_gettextはgettext.shに含まれる関数ですが、このままでは文字列としか扱われてないので、バッククォート「`」もしくは「$(」と「)」でeval_gettextと文字列をくくり、関数として実行されるようにします。そして「$(」と「)」の外をダブルクォーテーションでくくります。

#!/usr/bin/env bash
# hello.sh -- say hello script
. gettext.sh
echo "$(eval_gettext "hello world")"

potファイルの生成

xgettextを使って(bash --dump-po-stringsは使わない)メッセージカタログの元になる、potファイルを生成します。「-o」オプションで出力ファイル名をつけて、xgettextを実行します。

$ xgettext -o hello.pot hello.sh

$ cat hello.pot

# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-03-28 22:25+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#: hello.sh:4
#, sh-format
msgid "hello world"
msgstr ""

メッセージカタログを作成

potファイルをコピーし、言語のpoファイル(メッセージカタログ)を作成します。

$ cp hello.pot ja.po

適当なエディタを使い、ja.poをそれぞれの言語に翻訳します。 poeditなどの専用エディタならpoファイルの情報はそれぞれ適切に設定されますが、テキストエディタのみで作業する場合は、最低「Content-Type:」のCHARSETはUTF-8に設定してください。

$ cat ja.po

# hello ja.po
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-03-28 22:26+0900\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: Japanese\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" ← CHARSETをUTF-8にしておく
"Content-Transfer-Encoding: 8bit\n"

#: hello.sh:4
#, sh-format
msgid "hello world"
msgstr "こんにちは 世界" ←msgidに対応した文を翻訳する

poファイル(メッセージカタログ)のコンパイルと配置

poファイルをコンパイルしmoファイルを生成。メッセージカタログのディレクトリに配置します。

まずカレントディレクトリにmoファイルを置くディレクトリを作成します。

$ mkdir -p ja/LC_MESSAGES

poファイルをコンパイルします。moファイルの出力先は「-o」オプションで変更できるので、ja/LC_MESSAGES/(アプリケーション名).mo とします。

$ msgfmt -o ja/LC_MESSAGES/hello.mo ja.po

メッセージカタログの適用テスト

メッセージカタログが生成できたので、スクリプトにそれぞれの言語でメッセージカタログが適用されるかテストします。

通常、メッセージカタログは /usr/share/locale/ja/LC_MESSAGES (日本語の場合)以下に置かれますが、メッセージディレクトリを指す環境変数 TEXTDOMAINDIR と、アプリケーション名を指す環境変数 TEXTDOMAIN を変更するとカレントディレクトリを指定できるので、それぞれを変更してテストをおこないます。

LANGにCを設定した場合
$ LANG=C TEXTDOMAIN=hello TEXTDOMAINDIR=. ./hello.sh
hello world

LANGにja_JP.UTF-8(日本語)を指定した場合
$ LANG=ja_JP.UTF-8 TEXTDOMAIN=hello TEXTDOMAINDIR=. ./hello.sh
こんにちは 世界

それぞれ適用できました。

TEXTDOMAINDIRの設定とメッセージカタログのアップデート

スクリプトを実行する際、いちいち「TEXTDOMAIN=hello TEXTDOMAINDIR=.」をつけての実行は面倒なので、スクリプトの中に書いてしまいます。

#!/usr/bin/env bash
# hello.sh -- say hello script
. gettext.sh

TEXTDOMAIN=hello
export TEXTDOMAIN
TEXTDOMAINDIR=/home/hoge/src/hello/
export TEXTDOMAINDIR

echo "$(eval_gettext "hello world")"

TEXTDOMAINは変わりませんが、TEXTDOMAINDIRは、「.」のままではカレントディレクトリを示して、別のディレクトリからスクリプトを実行するとメッセージカタログが適用されないので、フルパスでスクリプトのディレクトリを指定します。

スクリプトが変更されたので、メッセージの位置が変わります。ですのでメッセージカタログもアップデートします。

xgettextでpotファイルの生成は同じですが、msgmergeを使ってja.poをアップデートします。

$ xgettext -o hello.pot hello.sh
$ msgmerge -U ja.po hello.pot

上のような些細な変更ではなくメッセージの追加などあれば、未翻訳の部分もあるのでpoファイルに未翻訳部分の翻訳も追加します。 追加が終わればpoをコンパイルします。

$ msgfmt -o ja/LC_MESSAGES/hello.mo ja.po

正しくメッセージカタログが適用されるかテストします。

英語
$ LANG=C ./hello.sh
hello world

日本語
$ LANG=ja_JP.UTF-8 ./hello.sh
こんにちは 世界

ディレクトリを変更してもメッセージカタログが適用されるかテストします。

$ cd ~/
$ LANG=C src/hello/hello.sh
hello world
$ LANG=ja_JP.UTF-8 src/hello/hello.sh
こんにちは 世界

これで問題なくメッセージカタログが適用されるようになりました。

文字列を非推奨の「$"..."」で囲んでいるスクリプトにxgettextを使うと

文字列をこれまで解説をした「"$(eval_gettext "...")"」ではなく、非推奨の「$"..."」で囲んでいるスクリプトに、xgettextでメッセージカタログを生成しようとすると警告されます。

$ cat hello-old.sh
#!/usr/bin/env bash
# hello.sh -- say hello script
echo $"hello world"

$ xgettext -o hello-old.pot hello-old.sh
hello-old.sh:3: 警告: 文法 $"..." はセキュリティ上の理由で推奨されません. 代わりに eval_gettext を使ってください

bash --dump-po-stringsでは何も出ません。

$ bash --dump-po-strings hello-old.sh > hello-old.pot

ということで、メッセージカタログの作成にはxgettextを使いましょう。

_ シェルスクリプトだけで空白で区切られた文字列から特定部分だけ抜き出し、入れ替えて表示

シェルスクリプトを使っていて空白で区切られた文字列「"AAA BBB CCC DDD EEE"」から2番目のBBBと5番目のEEEを取り出し、入れ替えて「EEE BBB」と表示したい場合、cutやawkを使うのもよいですが、いったん配列に入れると位置を指定して取り出すことができます。

1: T="AAA BBB CCC DDD EEE"
2: F=(${T})
3: echo "${F[4]} ${F[1]}"

EEE BBB

1行目: 空白で区切られた文字列「"AAA BBB CCC DDD EEE"」が変数Tに入ってます。

2行目: 配列に代入するとき、F=(A B C D E)のようにするとF[0]=A, F[1]=Bのように配列Fにそれぞれ分割されて代入されます。ここに空白で区切られた文字列$Tを指定すると、F[0]=AAA, F[1]=BBBのように分割されて配列Fに代入されます。

3行目: ということで、F[4]に文字列「EEE」、F[1]に文字列「BBB」が入っているので、それぞれ指定してechoで表示しています。