この記事はCodeIgniter Advent Calendar 2016の2日目です。

CodeIgniterだけでなくFuelPHPにも言えるのですが、言語クラス(Langクラス)でi18n対応するのは全くおすすめできません。PHPが提供しているgettextを使いましょう。

Langクラスとは

Langクラスを使うと、言語設定に合わせて読み込み対象とする文字列定義ファイルを変え、それによって国際化対応をしようとするものです。

具体的にはフォームバリデーションのエラーメッセージとかですね。これを日本語に書き換えて使う、というのは必須の作業と思います。よくCIを使われているなら手元に日本語ファイルを持っていると思いますし、本家で各国語を集めています(ただし本当にMITライセンスの条件を満たしているかは検証されていないように思います)。

言語設定は application/config/config.php の中にあります。また、ファイル読み込み時に言語を指定することできるので、ユーザ情報をもとに変更することもできます。

使い方は

$this->lang->line('form_validation_required');

のようにします。簡単ですね。

言語クラスを使うと何が起きるか

例えばECサイトのお届け先情報に「配送時間帯」を追加するとしましょう。「メールアドレス」欄の下に追加するとします。

こういう時に想像するコードというのは、こんな感じのコードだと思います。

<table>
  <tr>
    <th>名前</th>
    <td><?= $order->name ?></td>
  </tr>
  <tr>
    <th>都道府県</th>
    <td><?= $order->prefecture ?></td>
  </tr>
  <tr>
    <th>市区町村</th>
    <td><?= $order->city ?></td>
  </tr>
  <tr>
    <th>町名・番地</th>
    <td><?= $order->street ?></td>
  </tr>
  <tr>
    <th>建物名</th>
    <td><?= $order->building ?></td>
  </tr>
  <tr>
    <th>メールアドレス</th>
    <td><?= $order->email ?></td>
  </tr>
  <tr>
    <th>コメント</th>
    <td><?= $order->comment ?></td>
  </tr>
</table>

それがこうなります。

<table>
  <tr>
    <th><?= $this->lang->line('user.name') ?></th>
    <td><?= $order->name ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('address.prefecture') ?></th>
    <td><?= $order->prefecture ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('address.city') ?></th>
    <td><?= $order->city ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('address.street') ?></th>
    <td><?= $order->street ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('address.building') ?></th>
    <td><?= $order->building ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('user.email') ?></th>
    <td><?= $order->email ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('order.comment') ?></th>
    <td><?= $order->comment ?></td>
  </tr>
</table>

場合によってはこうなります。

<table>
  <tr>
    <th><?= $this->lang->line('msg0001') ?></th>
    <td><?= $order->name ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('msg1015') ?></th>
    <td><?= $order->prefecture ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('msg1016') ?></th>
    <td><?= $order->city ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('msg1017') ?></th>
    <td><?= $order->street ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('msg1018') ?></th>
    <td><?= $order->building ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('msg1013') ?></th>
    <td><?= $order->email ?></td>
  </tr>
  <tr>
    <th><?= $this->lang->line('msg1019') ?></th>
    <td><?= $order->comment ?></td>
  </tr>
</table>

最後のは極端ですが、エラーメッセージでやりがちです。

言語クラスのデメリット

視認性の悪さ:取り違えに気づきにくい

まずこの抜群な視認性の悪さに気力を奪われたことと思います。「メールアドレスの下ってどこだよ!」と言いたくなりますし、初期構築時も変数とラベルの取り違えに相当注意を払わないといけません。コードレビューが困難です。この例では変数は7つだけですが、実際にはほとんどのページで数多くを並べることになります。

ラベルが直接埋め込まれていれば、そんなことはなかったはずです。

未翻訳・変更追従に気づきにくい

文言を変更・追加する場合、母国語以外の翻訳先ファイルも文言変更が必要です。

プログラマと翻訳担当者が同じであれば、それほど大した話にはならないでしょう。作業者が全部翻訳してしまえばよいです。ですが、たいていは別の担当者です。
この際、どのラベルを変更したか・増やしたかを適切に伝達する必要があります。

「適切に伝達」。怖いですね。こういったものはすぐ破たんします。

翻訳定義ファイルのdiffを取りましょうか? 全行差分になるので追加はともかく変更は検知できません
ならばexcel管理しましょうか? なんとかできそうですが、だいぶ面倒ですね。

初期構築時はおそらく問題になりません。全部作り終わった後で言語ファイルを翻訳者に渡せばよいからです。運用開始後のメンテナンスが大変になります。

翻訳漏れが実際に発生すると、発覚が難しくなります。
Lang::get()Lang::line()は定義されていない文字列を要求された際、エラーログを取ってfalseを返します。FuelPHPの場合は第2引数(null)をデフォルトとして返しますが、ログは記録しません。
ログの有無の差はありますが、ともにechoすると空文字列になります。

「ラベルに漏れがないか」という確認(テストor文字単位のdiff)が必要になります。工数がすごく大きくなります。確認できるのはその言語を読める人だけです。

gettextを使う

PHPに限らず、一般的なi18n対応にはgettextが使用されます。gettextを使用すると次のようになります。

<table>
  <tr>
    <th><?= __('名前') ?></th>
    <td><?= $order->name ?></td>
  </tr>
  <tr>
    <th><?= __('都道府県') ?></th>
    <td><?= $order->prefecture ?></td>
  </tr>
  <tr>
    <th><?= __('市区町村') ?></th>
    <td><?= $order->city ?></td>
  </tr>
  <tr>
    <th><?= __('町名・番地') ?></th>
    <td><?= $order->street ?></td>
  </tr>
  <tr>
    <th><?= __('建物名') ?></th>
    <td><?= $order->building ?></td>
  </tr>
  <tr>
    <th><?= __('メールアドレス') ?></th>
    <td><?= $order->email ?></td>
  </tr>
  <tr>
    <th><?= __('コメント') ?></th>
    <td><?= $order->comment ?></td>
  </tr>
</table>

日本語で読めるって素晴らしいですね!
最初は英語だった、という場合でもこのようになります。

<table>
  <tr>
    <th><?= __('Name') ?></th>
    <td><?= $order->name ?></td>
  </tr>
  <tr>
    <th><?= __('Prefecture') ?></th>
    <td><?= $order->prefecture ?></td>
  </tr>
  <tr>
    <th><?= __('City') ?></th>
    <td><?= $order->city ?></td>
  </tr>
  <tr>
    <th><?= __('Street') ?></th>
    <td><?= $order->street ?></td>
  </tr>
  <tr>
    <th><?= __('Bldg') ?></th>
    <td><?= $order->building ?></td>
  </tr>
  <tr>
    <th><?= __('e-mail') ?></th>
    <td><?= $order->email ?></td>
  </tr>
  <tr>
    <th><?= __('comment') ?></th>
    <td><?= $order->comment ?></td>
  </tr>
</table>

まだ読めますね。__()のおかげでラベルがラベルであると視認できます。$this->lang->line()では長すぎて処理のように見えます。

翻訳時はxgettextコマンドで対象文字列を一括抽出できますので、プログラマにかかる手間は__()で囲むことだけです。プログラミングに集中することができ、言語ファイルを作る手間がありません。

一括抽出した文言はpoeditなどの翻訳ツールで管理できます。追加・変更・削除時も一括抽出して再取り込みすると、poeditが差分を未翻訳文言として表示してくれます。poeditは翻訳作業内容を翻訳ファイル(.poファイル/.moファイル)に出力します。

xgettextとpoeditの2つによってプログラマと翻訳者の完全な分業が可能になります

__()はよく使われるユーザ定義関数で、組み込み関数はアンダーバーが1つだけの_()です__()は自分で定義してください。翻訳ファイルを初期化する・ログインユーザにより言語を切り替えるなどの処理が必要になることでしょう。もちろんフレームワーク初期化のタイミングで行っても良いです。

未翻訳のものは原文のまま表示されます。翻訳されていなくても空文字列よりはよほど良いでしょう。

どうしてCodeIgniterは言語クラスを作った?

PHP組み込み機能のほうが優れている機能をどうしてフレームワークが用意してしまったか、というのは疑問となるところですが、これは「フレームワークとしての制約」ではないかと思います。

  • gettextはコンパイル時にオプション指定が必要なため、すべての環境に入っているわけではない(※それでも多くの環境に入っています)
  • ユーザがgettextの取り扱いを知っているわけではないが、バリデーションエラー文言など一部機能で多言語化の要求が必ず発生する

もともとは単にgettextの存在を知らなかったのかもしれません。ただ、インストール環境を選ばないというのがCodeIgniterの目標の1つなので、いまさら拡張機能を前提とした機能に変更することはできないのだと思います。

メンテナンスが大変という問題がありますが、フレームワークが表示する文言は僅かなので、なんとかなってしまいます。

プラグインを作る場合はフレームワークの仕組みに乗っかったほうが良いので、多くのプラグイン作者もこれに従うことになります。フレームワークが言語クラスを現在でも提供し続ける意味はプラグインにあると思います。

しかし、アプリケーション作者はこれに従うことはありません。より多くのノウハウが世にあるgettextを使いましょう。


次のアドベントカレンダー3日目はNEKOGETさんの「CodeIgniter2から3へのバージョンアップをしましょう(急いで!)」です。