PHPで多言語化:i18n(gettextの使い方)
以前に、Angularのプロジェクトを多言語化(i18n)しました。
それは@angular/localizeを使うもので、Angular内のメッセージの多言語化はこれで問題ないのですが、メッセージがバックエンドから来ている場合も多々あると思います。
そこで今回は筆者がAngularのバックエンドとして用いているPHPの多言語化をGUN gettextを使ってしてみたいと思います。
gettext
翻訳に利用するパッケージは「GNU gettext」です。
これはAngular/localize同様に、翻訳したい言語とその訳をあらかじめ設定しておき、プログラム実行時にロケールに合わせてそれを読みに行くような仕組みになっています。
GNU gettextのページに豊富な英語のドキュメントがありますが、日本語の解説ではQuiita:「gettext のコマンドラインツールを使おう」が分かりやすかったです。
gettextはPHPだけでなく、他のプログラミング言語C++やrubyなど他の言語でも利用することができます。
筆者がよく使うDebianでは、初期のインストールでgettext-baseがインストールされていました。
このgettext-baseの方はいうならランタイムみたいなもので、すでに設定されたものを処理することはできますが、翻訳作業をやろうと思うとこどれだけではできません。そのためにはgettext本体が必要です。これはaptからインストール可能です。
コンパイル済みのWindowsのバイナリは、公式ページでも紹介されている通りgithubにホストされています。こちらの設定方法も後半で紹介したいと思います。
.moファイルと.poファイル
プログラムが参照する翻訳データの拡張子は.moです。Debianで見つけた /boot/grub/locale/ja.moをcatで出力してみると、バイナリデータの後に翻訳データと予想される可読データも含まれていました。
... [--no-mem-option] [--type=TYPE] FILE [ARG ...][-c FILE [-p PREFIX]] [FILE1 [FILE2 ...]][-e|-n] 文字列[-f FILE][-f FILE] variable_name [...][-f|-l|-u|-s|-n] [--hint HINT [--hint HINT] ...] NAME[-l|-h|-a] [FILE ...][ADDR|comUNIT][,SPEED][ARG][ENVVAR=VALUE][NUMBER:]VARNAME[OPTIONS] DISK[OPTIONS] FILE_OR_DEVICE[PATTERN ...][[年-]月-日] [時:分[:秒]]カーネルイメージを圧縮できません%s を開けませんdiskboot.img のサイズは %u バイトである必要がありますエラー: %s。 セクター順に整列していないデータがコアファイル中に見つかりましたコアファイルの最初のセクターがセクター順に整列していませんコアファイルのセクターがひどく断片化しています`%s' のサイズが %u ではありません`%s' のサイズが大きすぎます`%s' のサイ ズが小さすぎます2つの引数が必要ですサポートされていないシリアルポートのパリティ ですサポートされていないシリアルポートの速度ですサポートされていないシリアルポートのストップビット値ですサポートされていないシリアルポートのワード長です変数 ...
これを完全に可読なテキストデータに変換するには、「msgunfmt」を使います。通常は標準出力に、-oオプションで保存先を指定すると指定したファイルにデータを出力します。
# msgunfmt /boot/grub/locale/ja.mo -o ./sample.po cat sample.po ... msgid "unsupported serial port parity" msgstr "サポートされていないシリアルポートのパリティです" msgid "unsupported serial port speed" msgstr "サポートされていないシリアルポートの速度です" msgid "unsupported serial port stop bits number" msgstr "サポートされていないシリアルポートのストップビット値です" msgid "unsupported serial port word length" msgstr "サポートされていないシリアルポートのワード長です" msgid "variable `%s' isn't set" msgstr "変数 `%s' が設定されていません" msgid "you need to load the kernel first" msgstr "まずカーネルをロードする必要があります"
これで予想がつくように msgidに変換前の文字列、msgstrに変換後の文字列を設定し、空行で区切っています。
他にも#でコメントが入れられるようになっていますが、#に続く1文字目で意味付がされています。それらは次のようなルールになっています。.moから.poに変化する際はこれらのコメントは除去されてしまいます。
- # 翻訳者のコメント
- #. 抽出されたコメント
- #: 参照
「#: guide.awk:4」等、翻訳対象の文字がどこにあるかを示します。
- #, フラグ
これだけコメントではなく、後述のmsgfmtプログラムにより使用されます。
たとえば「c-format」フラグは、未翻訳の文字列と翻訳がC形式の文字列であることを意味します。
先の.poファイルの出力に%sというディレクティブ(文字列を上流から取得する部分)がありましたが、これをそのまま文字列として解釈したいような場合に「%s」はcのフォーマットではないという意味で「no-c-format」フラグを付けたりします。
- #| msgid 前回の変換前の文字列
.poファイルのmsgstrを翻訳情報を変更して再び.moファイルにしたい場合は「msgfmt」コマンドを使います。 msgfmtで対象の.poファイルを指定しますが、msgunfmtの時と違って-oオプションを使って保存先を指定しない場合はmessages.moというファイルをカレントディレクトリに出力します。
もうひとつ今回使うコマンドに「xgettext」があります。これはプログラムのソースコードから翻訳部分だけが空白となっている.poファイルを生成するコマンドがあります。この使い方は後から紹介します。
gettextのコマンドは他にもいろいろとあるようですが、今回使うのは以上です。
ロケールの確認と設定
gettextを、Debian11上のPHPで動かしてみたいと思うのですが、そのまえに現在のロケールを確認します。
Dbianでは「locale」コマンドで現在のロケールを表示できます。
$ locale LANG=ja_JP.UTF-8 LANGUAGE= LC_CTYPE="ja_JP.UTF-8" ...
また、 Hatena Blog:「PHPでgettextする際の注意事項というか、setlocaleの罠」によれば、DebianだとOSに設定されていなロケールは使用できないとうことでした。
そこで、「locale -a」コマンドで利用可能なロケールも確認しておきます。
$ locale -a C C.UTF-8 POSIX ja_JP.utf8
テスト用に「dpkg-reconfigure locales」を実行して「en_US.UTF-8」(英語)「de_DE.UTF-8」(ドイツ語)を追加します。このコマンドは管理者権限で実行する必要があります。
PHPでロケールの変更をするには、setlocaleを使います。
またシステムによっては環境変数の変更も必要で、その際はputenvも使います。putenvを設定する場合は、setlocaleの前に記述します。
sample.php
...
putenv('LC_ALL=de_DE.UTF-8');
setlocale(LC_ALL, 'de_DE.UTF-8');
echo setlocale(LC_ALL, 0);
...
setlocaleコマンドは使用環境に依存するようで、ロケールとして渡す文字列もそれに合わせる必要があります。この戻り値は成功時にはセットされたロケール、失敗時はfalseが返ります。
また、PHP:「setlocale」にあったUser Notesには、現在のロケールを取得する方法として、setlocaleの第二引数に0を渡す方法が照会されていました。
ドメイン
gettextにはドメインという概念があります。これは、ロケールと一緒に翻訳データの識別子となります。
PHPではそのドメインと、翻訳データの保存先ディレクトリの指定を次のようにします。
sample.php
...
//ドメイン別の保存先の設定
bindtextdomain("ドメイン名", "./locale");
//ドメインの設定
textdomain("ドメイン名");
...
上では「ドメイン名」に対し、localeディレクトリを設定しましたが、実際の運用ではこれにロケールによる識別が加わります。
翻訳用のファイルを保存する場所は「./locale/ロケール/LC_MESSAGES/ドメイン名.mo」となります。
ロケールの部分にはja_JPやde_DEが入ります、またLC_MESSAGESの部分は固定文字です。
ちなみに、筆者yの環境では(Dbian11)において、setlocaleを実行せずに日本語訳(ja_JP)の設定を適用させようと思ったのですが、これはできませんでした。先の方法でデフォルトのロケールを確認したところ、次のような値になっていて、おそらくこの値ではうまく適用されないのではないかと予想されます。
PHPでのマーキング
今度はPHPソースを多言語化用に設定します。
PHPでは翻訳したい言葉をgettext関数でラップします。またこの関数のエイリアスには_(アンダーバー)が用意されているので、_での代替も可能です。
このgettext関数は目的の翻訳が見つからない場合は、引数の文字列をそのまま返します。
次のようなコードを作成してみました。引数に名前をもって挨拶をするプログラムです。
PHPの場合プレースホルダ(%s)を利用するにはsprintfに当てはめます。
sample.php
...
$strUserName="unknown";
if (isset($argv[1])){
$strUserName=$argv[1];
}
echo gettext("This is test")."\n";
echo _("hello!")."\n";
echo sprintf(_('Wellcome, %s'),$strUserName)."\n";
...
コードができたら、翻訳用のテンプレートファイルを作成します。ここでさきほどの「xgettext」を利用します。
-Lオプションで、言語にPHPを指定、 --from-codeオプションで文字コードをUTF-8に指定しています。
出来上がったsample.poは次のようになっています。
sample.po
# 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: 2022-06-06 21:10+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=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: sample.php:12
msgid "This is test"
msgstr ""
#: sample.php:13
msgid "hello!"
msgstr ""
#: sample.php:14
#, php-format
msgid "Wellcome, %s"
msgstr ""
ソースコードの文字形式が判別できなかった場合にそうなると思いますが、下線部のUTF-8の部分がCHARSETとなることがあります。そのままの状態だと「警告: 文字セット "CHARSET" は汎用のエンコーディグ名ではありません.ユーザの文字セットへのメッセージの変換はうまく働かないかも知れません.」という警告が出ますので、その際はUTF-8に修正します。またファイルの保存形式もUTF-8にする必要があります。
msgstrの部分を修正して保存します。
sample.po
...
#: sample.php:12
msgid "This is test"
msgstr "テストです"
#: sample.php:13
msgid "hello!"
msgstr "こんにちはー"
#: sample.php:14
#, php-format
msgid "Wellcome, %s"
msgstr "ようこそ %s さん"
前述のmsgfmtコマンドで.moファイルにしたあと、フォルダを作って配置します。
# msgfmt sample.po -o sample.mo # mkdir -p locale/de_DE/LC_MESSAGES/ # cp sample.mo locale/de_DE/LC_MESSAGES/
ドメインの値と、putenvとsetlocaleの設定値に合わせて翻訳変換されます。setlocaleをPHPで設定するには.UTF-8が必要ですが、筆者が試してみたところディレクトリの設定は.UTF-8がついててもいなくても翻訳されました。
また、PHP側の文字列を抽出時から変更してしまうと、翻訳対象となりません。その際は.poファイルの文字列をPHPで変更したのと同じように変更して再度.moファイルにして配置する必要があります。
# php sample.php Nanbu テストです こんにちはー ようこそ Nanbu さん
Windows環境への導入
WindowsのPHP環境ではおそらくデフォルトでgettext関数は使えません。先のようなgettext関数のあるPHPを動かしてみると次のようなエラーがでると思います。
そこで、php.iniに追加のextensionを指定します。いままで拡張を利用していなかった場合は、拡張フォルダへのパスの設定(コメントアウトを解除)も必要です。
php.ini
... # コメントアウトされていたら解除 # 筆者の環境では、Apache連携時は絶対パス(c:/php8/ext)にしないと動きませんでした extension_dir = "ext" # Dynamic Extensionsの最後に追加 extension=gettext ...
これで使えるようにはなるはずなのですが、次のような問題に遭遇しました。Windows10環境です。
- setlocaleにセットする文字列が違う
PHPのgettextのページにあったように、setlocaleに空文字を渡すとシステム変数をセットしてくるようです。そこで得られた結果は「Japanese_Japan.932」という文字列でした。
このルールに従って「French_France.65001」(65001はUTF-8のコードページ)を設定したところlocaleの変更には成功しました。
また、fr-FRやde-DEとハイフン版でロケールを渡してやると、それと認識するようです。
これらの設定をputenvとともに記述しても、結局ja_JPのデータを参照するようです。
- ロケールと保存先フォルダの名前が違う
setlocaleでは「Japanese_Japan」という値ですが、保存先のフォルダは「ja_JP」となるようです。
- setlocaleで設定したロケールが無視される
putenvと合わせて使ってみたのですが、setlocaleで設定したロケールは無視されるようでした。
試行錯誤の結果、常に表示言語の設定に依存するような感じでした。たとえば次の画面でフランス語を設定した場合「fr_FR」の設定が適用されます。
表示言語の設定変更後にログインパスワードが一致しなくなる症状がでることがありますので注意してください。これはロケールの切り替えによってキーボードの配列が変わってしまうのが原因で、入力文字を確認しながら正しい文字を入力すれば問題ありません。
または、ログイン画面からでも入力方式を切り替えられますので、右下のIMEアイコンから入力方式を日本語にすれば元通り入力できます。
.poファイルや、.moファイルの編集に関しては前述の通り、github:mlocati/gettext-iconv-windowsよりバイナリをダウンロードしてきます。
種類がアーキテクチャ別とshared/staticとあります。sharedはdllを必要とするかわりにサイズの小さいものとなっています。staticはdllには依存しませんがサイズが大きいです。ここではstaticのzip版をダウンロードしました。
作業方法はDebianの時と同じです。注意としてはzip版はコマンドにパスが通っていないので、バイナリフォルダへパスを通すか、ファイルを展開したフォルダ内にあるbinフォルダに移動して作業してください。
参考にさせていただきましたサイトの皆様、ありがとうございました。