WHAT'S NEW?
Loading...

CakePHPで多言語対応をする 【2/3】

言語切替ボタンを付けてみようじゃないか。

最終的にはフリーな国旗アイコンなどをゲットして、好きなレイアウトにすれば良い話なので、とりあえず全部テキストでやっていく予定。

完全にどのページからでもいつでも言語を切り替えられるのかと言われれば、「正解です」といえる様な処理なんだけど、ページネータなどのnamedパラメータが絡むテストはしていないので予めご了承していただきたい。
何事もそうだけど、「この処理は完全に全ページで正しく動くのか?」とか「1箇所でも挙動が怪しい場合は実装するな!」とか、そんな予測コストなんかはっきりいって犬が喰えば良い

なんかあった時のことを考えつつも、「こうなる可能性もあるが、ここはひとまずこう実装しておく」という、対応コストで作っておかないと、あなたは何時まで経っても自宅に帰れなくなる

そして「そんな事いってもそういう仕組で仕事が進んでいるんだからしょうがないじゃないか」と言う人がいるかも知れないが、それはそういう星の下に生まれたことを己が悔やむしかない。そしてもっと良い会社に転職できることを(俺は本当に)切に願う。

大企業のクソフローなんか無視してなンぼ。
というわけでひとまず、新規でCakePHPを構築してみることにする。
  1. CakePHPのダウンロードと設置後、最低限の設定
  2. Dashboardsコントローラ
  3. POファイル作成
  4. 言語を変更
  5. 言語切替処理
  6. リファラを使う
だいたいこういった内容と流れになる予定だ。

当然だがXAMPPなどのLinux詐欺な環境での動作確認は一切してないので、こちらもあわせてご了承いただきたい。あくまで純粋なLAMP環境を前提としている。

もしLinux詐欺をやめて、正しいLinuxを使いたくなった場合、VMwareを使うと良い。
詳しくはこちらを参照されたし。

さて早速取り掛かろう。

■CakePHPのダウンロードと設置後、最低限の設定

今回は会えて1.2を使う。というのも、1.3は1.2と比べると変更点が多く、まだ俺自身調査しきれていないからだ。

世の中には便利な本もある。『Pocket詳解 CakePHP辞典』だ。
俺はまだこの本は手に入れて無い。しかしこの本は正直欲しい。そのうち買う。

マロはどうしても1.3じゃなきゃイヤじゃ!的なわがままちゃんは、こういった優れた書籍をゲットし、己で修行していただければと思う。

さて、CakePHPの1.2.8をダウンロードしたとして、Apacheの設定も終え、早速初期設定といこうじゃないか。

まずは余計なファイルの削除と、最低限の設定。



【パーミッションの設定】
/app/tmpをまるごと書き込み可能にしておく。
# cd /path/to/cakephp/app/
# chmod -R 707 tmp

【ハッシュ値の生成】
以下のようなコマンドで簡単にハッシュ値を生成できる。
$ mkpasswd -l 40 -s 0
これで生成されたハッシュ値をcore.phpのSecurity.saltに上書きする。

【データベースの設定】
database.php.defaultをdatabase.phpにコピーし、中身を書き換える。
今回特にデータベースは使わないが、そのうち使うようになるので、今の段階でやっておく。
適当に適切な内容に適宜書いておくこと。

【アセットの削除】
これは個人的な好みの問題なので、正直やらなくても良いが、俺は以下のファイルをすべて削除している。
  • 各ディレクトリに存在するemptyファイル
  • /app/webroot/img/内のロゴ画像
  • /app/webroot/css/内のcssファイル
【レイアウトファイルの準備】
/app/views/layout/内にdefault.ctpとしてファイルを設置し、お好きな(X)HTMLを記述しておく。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<title><?php __('Test of Internationalization')?> | <?php echo $title_for_layout?></title>
</head>
<body>
<h1><?php __('Test of Internationalization')?></h1>
<?php echo $content_for_layout?>
</body>
</html>
こんな感じ。

※アセットの削除とあわせて説明するが、個人的にCakePHPのデフォルトのデザインがあまり好きではないので俺はこのような余計な処理をしているが、はっきりいってただの好みの問題なので、ここらへんは好きにするのが良いと思う。
だけど、デフォルトデザインのままリリースすることなんぞめったに無いと思うし、こういったことは最初にやっておくのが良いと思うわけだ。


【app_controller】
/app/直下にapp_controller.phpを設置する。
中身はこのような感じ。
class AppController extends Controller {

  public $helpers =     array('Html');
  public $components =  array();
  public $uses =        array();

  public function beforeFilter()
  {
    parent::beforeFilter();
  }
}
さて、ここで何をやっているのかというと、別に何もやってない。
いろいろな良書、そして無数にあるCakePHPのブログをかたっぱしから読んでいる人に対して、俺がこの時点で何か説明するような事は何一つ無い。

app_controllerの存在意義を理解している人は、これからここにゴリゴリとコードが書かれていくのをいとも簡単に想像できるはずだ。

【routes.php】
いまからダッシュボードというコントローラを作り、デフォルトのドキュメントルートで表示させるので、ルーティングを設定しなおす必要がある。

該当ファイルは/app/config/routes.php だ。

"/"が指定された場合の処理を以下のように変更する。
Router::connect('/', array('controller'=>'dashboards', 'action'=>'index', 'home'));

さて、これで下準備が完了したので、早速CとかVとかを作っていこうじゃないか。

■Dashboardsコントローラ

【ダッシュボード】
/app/controllers/にdashboards_controller.phpを作る。
中身は全然たいしたことはなく、ただ「コントローラ作っておかないとアクセスできないズラ!」対策のようなものだ。
class DashboardsController extends AppController {
  public $name = 'Dashboards';
  public $uses = array();

  public function beforeFilter()
  {
    parent::beforeFilter();
  }

  public function index() {}
}
そしてビューも作っておく。
今回$usesで空配列を指定しているのでモデルは作らなくて良い。

ビューは例によって、/app/views/dashboards/index.ctpを作る。
__('Dashboards')
なぜ日本語じゃなくて英語にするか、だけど、これにはいろいろワケがある。
以下のような流れを経験し、俺は英語で書くのが楽かもしれないと思っている。
  1. 最初は日本語で書いていた
  2. 後からi18n機能を使って英語を作れば良いと思っていた
  3. しかし日本語1つに対して英語が複数当てはまる場合があることを知る
  4. 日本語はダブル/トリプルミーニング多いが、他の言語ではそれほどでもない
  5. だったら最初から英語にしておいた方が良いかも
例えば「10日」「20日」などの"日"を書いておいたとする。
__('日');
そしてこれをPOEditで翻訳する際、なんと、日曜日の省略形「日」と、日付の「日」が同じということに気がつく。
そしてどちらかしか指定が出来ない。ウホ!
※実際にカレンダーなど作ってみればわかるが、もっと複雑な不具合が多発するだろう

であれば、単語ではなく文章にしておけば良い。
echo sprintf(__('%1$02d日から%2$02d日まで',
  $this->data['Reserve']['begin'],
  $this->data['Reserve']['end']);
これならまぁ及第だろう。しかし、別に「日」だけではない。
「月」だってそうだ。

そして俺達はこんなことをいちいち考えながらやるのがアホらしいと思っている。
だったら最初から英語かな、と思ったわけだ。

ただし、俺はそんなに英語が出来ない。
日本語>英語>日本語というように、Google翻訳でいちいち試しながら訳しているだけにすぎず、完全に英語は不得意分野として認識している。

となるとやっぱり「日本語で、文章で書けばいいじゃん」となるが、個人的に英語を覚えたいので、無理やり英語でやることにする。
何事もやればできるし、出来るまでやらないと文句言えないだろう。

あなたは別に俺に合わせる必要は全くないので、タガログ語でもエスペラント語でも赤表紙のエルフ語でも、好きな言語をデフォルトとして選ぶが良い。※ただしISOに無い言語は死ぬほど苦労するだろう

というわけで話がそれたが、英語でgettext形式で記述してみた。
この状態でブラウザでアクセスすると、画面にはビューファイルに書いたとおり、英語で「Dashboards」と表示されるはずだ。


全く普通の、想定できる結果だ。何も問題無い。

これをまず日本語に翻訳するところから始める。

■POファイル作成
コンソールで作業する。
まずは/path/to/cakephp/cake/console/までcdで移動してから、以下のコマンドをうつ。
$ ./cake i18n -app /path/to/cakephp/app
これで、すでに存在するファイルから勝手にgettext形式を抜き出し、potファイルを生成してくれる準備ができた。
---------------------------------------------------------------
I18n Shell
---------------------------------------------------------------
[E]xtract POT file from sources
[I]nitialize i18n database table
[H]elp
[Q]uit
POTファイルを抽出するので、「E」をタイプ+Enterする。
What is the full path you would like to extract?
Example: /home/cakephp/cake_lang/myapp
[Q]uit  
[/path/to/cakephp/app] > 
抽出元はどこですねん?と聞かれているので、ここは素直にそのままEnterする。
What is the full path you would like to output?
Example: /home/cakephp/cake_lang/app/locale
[Q]uit  
[/home/cakephp/cake_lang/app/locale] > 
出力先はどこですねん?と聞かれたので、localeでOKなのでこのままEnterする。
Extracting...
---------------------------------------------------------------
Path: /path/to/cakephp/app
Output Directory: /path/to/cakephp/app/locale/
---------------------------------------------------------------
Would you like to merge all translations into one file? (y/n) 
[y] > 
すべての翻訳ファイルを1個にまとめるかと聞かれるのでy+Enterする。
※nだと個別ファイルがディレクトリに残るので一度試して確認してみると良い

そして以下のように表示される。
Processing /path/to/cakephp/app/app_controller.php...
Processing /path/to/cakephp/app/index.php...
Processing /path/to/cakephp/app/config/acl.ini.php...
Processing /path/to/cakephp/app/config/bootstrap.php...
Processing /path/to/cakephp/app/config/core.php...
Processing /path/to/cakephp/app/config/database.php...
Processing /path/to/cakephp/app/config/inflections.php...
Processing /path/to/cakephp/app/config/routes.php...
Processing /path/to/cakephp/app/config/sql/db_acl.php...
Processing /path/to/cakephp/app/config/sql/i18n.php...
・・・
Done.
抽出が完了したらしい。

そしたら出力された/app/locale/default.poを、/app/locale/jpn/LC_MESSAGES/default.poとしてコピーする。
途中のディレクトリ(jpn/LC_MESSAGES/)は、なければ作成しておくこと。

もちろんあなたがスペイン人で、最初に英語をスペイン語に翻訳したいと考えているのであれば、/app/locale/jpn/LC_MESSAGES/default.poではなくて/app/locale/spa/LC_MESSAGES/default.poとなるようにコピーするべきだ。そうしないとちゃんと表示されない。そもそもCakePHPがそういう仕組なのみだから。

そして翻訳編集はこの/app/locale/jpn/LC_MESSAGES/defautl.poを使う。

POEditを使っても良いが、今回はとりあえずテキストエディタで開き、直接書いてみようと思う。
#: /views/dashboards/index.ctp:1
msgid "Dashboards"
msgstr ""

#: /views/layouts/default.ctp:8;11
msgid "Test of Internationalization"
msgstr ""
ここのmsgstr の中身が空になっているので、日本語で「ダッシュボード」と書き、保存する。
ついでにタイトルも「国際化のテスト」などとしておこう。

以下のようになったはずだ。
#: /views/dashboards/index.ctp:1
msgid "Dashboards"
msgstr "ダッシュボード"

#: /views/layouts/default.ctp:8;11
msgid "Test of Internationalization"
msgstr "国際化のテスト"

さて、これでPOファイルは完了した。
この状態で、ブラウザをリロードしてみよう。

なんと、「Dashboards」が「ダッシュボード」と表示されたではないか!!ギョ!ビックリだ!!


どうしてこうなるのかというと、これはつまり前回説明したように、CakePHPがブラウザの言語情報を取得してくれるから可能になっているわけだ。

実際には/cake/libs/l10n.phpを見るとわかると思うが、Config.languageも関係している。
※L10nを読み込んでないのに国際化可能なのはまだちゃんと調べてない

どうだろうか?オモロイ!と思ったであろう?俺もオモロいと思った。ウホホ!
というわけで次に行く。

■言語を変更

CakePHPの自動で国際化してくれる機能はありがたいのだが、このままでは日本語で固定になってしまう。

やっぱり現地に行かないとだめなのか?
しかし現実は甘くない。

これを口実に海外出張なんかさせてもらえるわけはない。東南アジアのカフェでマンゴ-ジュースでも飲みながらカフェコーディング汁!とか思ったとしても、すぐに瓦解する甘い夢であることに気づくだろう。

その理由は、単にブラウザのロケールを変更するか、CakePHPのL10nで言語を変更できてしまうからだ。

というわけで、早速やってみよう。

まずL10nを読み込まないといけない。
app_controller.phpのbeforeFilterを以下のように変更してみる。
public function beforeFilter()
  {
    parent::beforeFilter();

    App::import('Core', 'L10n');
    $this->L10n = new L10n();
  }
これで常にL10n機能が使えるようになった。
この時点でブラウザを更新しても、特に何も変わらない。試しにやってみると良いだろう。

次に、このL10nを使って言語を英語にしてみる。
今表示されているのはDashboardsControllerのindexアクションだ。なので、dashboards_controller.phpのindex()メソッドを変更して確認してみることにする。

ここに以下のようにget()メソッドでロケールを与えてみよう。
public function index()
  {
    $this->L10n->get('eng');
  }
そしてこの状態でブラウザを更新すると、あれま!!英語に戻ったニダ!!


ということは、このgetメソッドの中身を動的に変更すれば、もう俺達は言語切替ボタンの為に夜中うなされることはないのではないだろうか?
いやきっとそうだろう。

■言語切替処理

では一旦、DashboardsControllerのindexに記述した、$this->L10n->get('eng');は削除しておこう。
削除するとブラウザでは日本語の表示になるはずだ。

日本人の俺達が最初にアクセスしたときに、画面の文字が日本語であることに異議を唱える奴なんぞいやしないし、この日本語表示の状態をデフォルトとして考える。

まずは言語選択用にelementを作成してみよう。
/app/views/elements/languages.ctpなんて名前にしておく。
<ul>
  <li><?php echo $html->link(__('日本語', true), 'lang:ja')?></li>
  <li><?php echo $html->link(__('Engish', true), 'lang:en')?></li>
</ul>

これを/app/views/layouts/default.ctpの中で読み込むことにする。bodyタグ内を以下のようにしてみた。
<body>
<h1><?php __('Test of Internationalization')?></h1>
<?php echo $this->element('languages')?>
<?php echo $content_for_layout?>
</body>
この状態でブラウザで見てみると、以下のようになるはずだ。


で、「日本語」もしくは「English」のテキストリンクにマウスを乗せると、URLがおかしなことになっているのに気がついたら偉い。何もあげないけど、褒めてあげる。

/dashboards/lang:ja や
/dashboards/lang:en になってしまっているではないか。

この場合、「lang:ja」と「lang:en」がアクション名と認識されてしまうので、クリックすると、「そんなアクションねぇズラ!」とCakeさんに怒られてしまう。

試しにやってみよう。ほら怒られた。



じゃーどうすればいいのよダーリンと、耳元で優しくささやいてくれる女子なんぞ俺のそばにはいないのだが、とりあえず優しさを忘れるのはまずい。女というのは優しく対応しておかないとあとが怖そうだ。別にCakeさんが女っつーわけじゃないが、言う言聞かないっつー意味ではそれほど相違は無いだろう。

対策としては、コントローラ名とアクション名を含んだ状態でリンクさせる、ということになる。
幸いにもビューファイル内でコントローラ名、アクション名を拾うことができるので、そのまま実装してみる。

が、コントローラ名は頭1文字が大文字になっているので、php関数のstrtolowerで小文字にしておく。
<ul>
  <li><?php echo $html->link(__('日本語', true), DS.strtolower($this->name).DS.$this->action.DS.'lang:ja')?></li>
  <li><?php echo $html->link(__('Engish', true), DS.strtolower($this->name).DS.$this->action.DS.'lang:en')?></li>
</ul>
記述が長くなったので、適宜改行を入れておくか、app_controller.phpのbeforeFilter内で、1個の短い変数にsetしておくのも手だ。そこら辺はあなたのセンスに任せる。

さてこの状態でクリックすれば、Cakeさんには怒られない。
怒られないが、何も変わらない。

そりゃそうだ。何も作ってないし。
というわけで、言語切替部分をちゃんとつくろうじゃないか。

基本的な考え方としては以下のような流れになる。
  1. namedパラメータが送られてきた時の処理
  2. セッションに言語が設定されていた時の処理
  3. 言語をセット
この順番で処理すればまぁ問題ない。
というわけで早速やってみよう。

どのコントローラからでも言語設定をさせたいので、編集するファイルはapp_controller.phpだ。
以下のようにプライベートメソッドを追加する。
private function __setLang()
  {
    if(!empty($this->params['named']['lang'])) {
      $lang = $this->params['named']['lang'];
    } else if($this->Session->read('lang')) {
      $lang = $this->Session->read('lang');
    }

    if(isset($lang)) {
      $this->L10n->get($lang);
      $this->Session->write('lang', $lang);
    }
  }
メソッド名は別に何でもいい。__setLanguage()でも良いが、__setGengo()とかのマヌケな名前は俺が下痢するのでぜひやめていただきたい。
※プライベートメソッドなので、アンダーバー2個を先頭に付けておこう。1個の場合はprotectedだ。

さて、これは何をやっているのかというと、まずは条件式で、namedパラメータに言語がセットされているのかどうかを見ている。つまり先程作成したelementのリンクをクリックしたのかどうか、を判定している。

言語名がクリックされたらlangというnamedなパラメータに入るので、それを一旦$langという変数に代入しておく。
そして、namedなパラメータがない場合、もしかしたらセッションに入っているかも知れないので、それを調べておく。
入っていたらこちらも$langに入れておく。

つまり$langの中に言語名を入れるのが役目だ。

そして後半、$langの中身を言語名として、L10nのgetで指定することにより、画面の言語を切り替えるという流れになる。
ただし初回は$langには何も入らないので、isset()で変数自体の存在をチェックしている。

なぜこういう仕組みにしたかというと、ブラウザデフォルトの言語を扱うためだ。
変にデフォルト時の言語を処理してしまうと、初回アクセス時は必ず日本語になってしまったりするので、海外在住の海外の奴(つまり外人だ)は困ること請け合いなしだ。

むしろ困らせてやるのも奴らのためになるのかも知れないが、それによって己の小ささを他人が笑うだけなので、やめておこう。仲良くしておくことをすすめる。

さて、次に、このプライベートメソッドを同じくapp_controller.php内のbeforeFilterから呼び出し、いつでも言語を切り替えられるようにしてみようじゃないか。

public function beforeFilter()
  {
    parent::beforeFilter();

    App::import('Core', 'L10n');
    $this->L10n = new L10n();
    $this->__setLang();
  }

さて、ブラウザを更新し、言語切替リンクをクリックしてみると良い。
気持ち良いくらい快適に言語が切り替わるのをその目で見れたはずだ。


■リファラを使う

で、このままだとまぁ一応言語は綺麗に切り替わるし、poファイル作るの面倒くさいなぁとか思いつつ、さりげなくURLを見てアボーーーンとなるだろう。namedパラメータがそのまま残ってしまっているのだ。

せっかくroutes.phpでルーティングの設定をしたのにもかかわらず、ロケーションバーに/dashboards/index/lang:enとか表示されてしまうのは寝覚めが悪いし、リリース後に味噌汁に毒を入れられたかのような冷や汗をかく瞬間を体験することになるかも知れない。

ここは一丁、URLもちゃんと綺麗にすっきりさせることにしようじゃないか。

実は意外に難しくない。
先程の流れだと
  1. namedパラメータが送られてきた時の処理
  2. セッションに言語が設定されていた時の処理
  3. 言語をセット
だが、この流れを
  1. namedパラメータが送られてきたら処理してリダイレクト
  2. セッションに言語が設定されていた時の処理
  3. 言語をセット
とすればよい。変えるのは1個目だけだ。
private function __setLang()
  {
    if(!empty($this->params['named']['lang'])) {
      $lang = $this->params['named']['lang'];
      $this->L10n->get($lang);
      $this->Session->write('lang', $lang);
      $this->redirect($this->referer());
    } else if($this->Session->read('lang')) {
      $lang = $this->Session->read('lang');
    }

    if(isset($lang)) {
      $this->L10n->get($lang);
      $this->Session->write('lang', $lang);
    }
  }
この状態でブラウザを更新すると、と言いたいけど、ロケーションバーが/dashboards/index.lang:enとかになっているとループしてしまうので、一旦/にしてから更新しよう。

理由は、リファラでリダイレクトしているからだ。言語変更のリンクで/dashboards/index.lang:enに飛んでからリファラで戻ってるのに、リファラが/dashboards/index.lang:enだとループする、ということだ。

まずはURLをドキュメントルートにしてから更新してみる。
その後、言語切替リンクをクリックすると、ちゃんとURLが変更されないで言語だけ入れ替えることができる。


ここまでのソースは以下。

app_controller.php
class AppController extends Controller {
  public $helpers = array('Html');
  public $components = array();
  public $uses = array();

  public function beforeFilter()
  {
    parent::beforeFilter();

    App::import('Core', 'L10n');
    $this->L10n = new L10n();
    $this->__setLang();
  }

  private function __setLang()
  {
    if(!empty($this->params['named']['lang'])) {
      $lang = $this->params['named']['lang'];
      $this->L10n->get($lang);
      $this->Session->write('lang', $lang);
      $this->redirect($this->referer());
    } else if($this->Session->read('lang')) {
      $lang = $this->Session->read('lang');
    }

    if(isset($lang)) {
      $this->L10n->get($lang);
      $this->Session->write('lang', $lang);
    }
  }
}

dashboards_controller.php
class DashboardsController extends AppController {
  public $name = 'Dashboards';
  public $uses = array();

  public function beforeFIlter()
  {
    parent::beforeFilter();

  }

  public function index() {}

}