laravel-5-boilerplateの言語切り替え処理


laravel-5-boilerplateを触ってみたら言語切り替え処理が実装されていたので解析してみました。

[PHP - laravel-5-boilerplateのインストール - Qiita](http://qiita.com/shiichi/items/954539dcf683e335f92d)

#laravel-5-boilerplateの言語切り替え処理
laravel-5-boilerplateのindexで、「Language」の項目選択で言語切り替えが実装されています。

該当箇所のHTMLソースを確認すると、

```HTML
<li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">Language <span class="caret"></span></a>
    <ul class="dropdown-menu" role="menu">
        <li><a href="http://laravel-5-boilerplate.app/lang/en">English</a></li>
        <li><a href="http://laravel-5-boilerplate.app/lang/es">Spanish</a></li>
        <li><a href="http://laravel-5-boilerplate.app/lang/fr-FR">French</a></li>
        <li><a href="http://laravel-5-boilerplate.app/lang/it">Italian</a></li>
        <li><a href="http://laravel-5-boilerplate.app/lang/pt-BR">Portuguese (Brazil)</a></li>
        <li><a href="http://laravel-5-boilerplate.app/lang/ru">Russian</a></li>
        <li><a href="http://laravel-5-boilerplate.app/lang/sv">Swedish</a></li>
    </ul>
</li>
```
となっています。

routes.phpはこう。

```php:app\Http\routes.php
<?php

/**
 * Switch between the included languages
 */
require(__DIR__ . "/Routes/Global/Lang.php");

（後略）
```

Lang.phpはこう。

```php:app/Http/Routes/Global/Lang.php
<?php

/**
 * Sets the specified locale to the session
 */
get('lang/{lang}', function($lang)
{
    session()->put('locale', $lang);
    return redirect()->back();
});
```

URLアクセスではセッションへの保存しか行っておらず、元のURLにリダイレクトされています。
表示時にここで保存したセッション情報を見て言語切り替えを行っているようです。

切り替え用の文字列はresources/lang/ディレクトリ以下に保存されています。
日本語への言語切り替えをする場合は、enをコピってjaを作れば良さそうです。


実際の所はどうやって切り替えをかけてるんだろう？というのを軽く調べてみると、
Kernel.phpでミドルウェアにLocaleMiddlewareを設定している箇所がありました。

```php:app/Http/Kernel.php
（前略）
    protected $middleware = [
        \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \App\Http\Middleware\LocaleMiddleware::class,
    ];
（後略）
```

app/Http/Middleware/LocaleMiddleware.phpはこうです。

```php:app/Http/Middleware/LocaleMiddleware.php
<?php namespace App\Http\Middleware;

use Closure;

/**
 * Class LocaleMiddleware
 * @package App\Http\Middleware
 */
class LocaleMiddleware
{

    /**
     * @var array
     */
    protected $languages = ['en', 'es', 'fr-FR', 'it', 'pt-BR', 'ru', 'sv'];

    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if(session()->has('locale') && in_array(session()->get('locale'), $this->languages))
        {
            app()->setLocale(session()->get('locale'));
        }

        return $next($request);
    }
}
```

セッションから拾ってきたlocaleをappにsetLocaleしています。
app()関数の戻り値は、bootstrap/app.phpでnewされているApplicationクラスのインスタンスです。

appのsetLocale、getLocaleはこうなっています。

```php:vendor/laravel/framework/src/Illuminate/Foundation/Application.php
（前略）
    /**
     * Get the current application locale.
     *
     * @return string
     */
    public function getLocale()
    {
        return $this['config']->get('app.locale');
    }

    /**
     * Set the current application locale.
     *
     * @param  string  $locale
     * @return void
     */
    public function setLocale($locale)
    {
        $this['config']->set('app.locale', $locale);

        $this['translator']->setLocale($locale);

        $this['events']->fire('locale.changed', [$locale]);
    }
（後略）
```

ここでsetLocaleされたlocaleは、ミドルウェアやパッケージで$app->getLocale()等として取得され、いろんな所で使われているようです。

中でも表示の切り替えを担当しているのはTranslationです。
vendor/laravel/framework/src/Illuminate/Translation/TranslationServiceProvider.phpで、

```php:vendor/laravel/framework/src/Illuminate/Translation/TranslationServiceProvider.php
（前略）
            $locale = $app['config']['app.locale'];

            $trans = new Translator($loader, $locale);
（後略）
```

としてTranslatorの参照先言語の指定に使われています。
Translatorの本体は、TranslationServiceProvider.phpの隣のファイル、Translator.phpです。
Translator.phpにはbladeに記述する{{trans}}の本体が居て、

```php:vendor/laravel/framework/src/Illuminate/Translation/Translator.php
（前略）
    /**
     * Get the translation for a given key.
     *
     * @param  string  $id
     * @param  array   $parameters
     * @param  string  $domain
     * @param  string  $locale
     * @return string
     */
    public function trans($id, array $parameters = [], $domain = 'messages', $locale = null)
    {
        return $this->get($id, $parameters, $locale);
    }
（後略）
```

となっています。

getメソッドは

```php:vendor/laravel/framework/src/Illuminate/Translation/Translator.php
（前略）
    /**
     * Get the translation for the given key.
     *
     * @param  string  $key
     * @param  array   $replace
     * @param  string  $locale
     * @return string
     */
    public function get($key, array $replace = [], $locale = null)
    {
        list($namespace, $group, $item) = $this->parseKey($key);

        // Here we will get the locale that should be used for the language line. If one
        // was not passed, we will use the default locales which was given to us when
        // the translator was instantiated. Then, we can load the lines and return.
        foreach ($this->parseLocale($locale) as $locale) {
            $this->load($namespace, $group, $locale);

            $line = $this->getLine(
                $namespace, $group, $locale, $item, $replace
            );

            if (! is_null($line)) {
                break;
            }
        }
（後略）
```

で、ここでファイルのロードと実際に表示する文字列の取得が行われています。

結構な長旅でしたが、ここまで分かれば言語情報の追加でどこを変更すれば良いのか一目瞭然ですね。

#まとめ：LocaleMiddleware、blade、resources/langの3箇所が要修正
- app/Http/Middleware/LocaleMiddleware.phpに、このアプリで使用する言語の一覧があるのでここを変更
- 切り替えのトリガとなっていたHTMLはresources/views/frontend/includes/nav.blade.phpに記述があるので、これも変更
- resources/lang/に各言語の文字列が保存されているので、ここに表示する文字列を記述。

の3箇所を触れば、言語切り替えが可能となります。

#問題点：初期表示が固定である、セッション保存のため保持期限が短い
boilerplateのこの実装では、初期表示がconfig/app.phpで設定した言語になってしまうので、ここはやはりブラウザが送出してくるLocaleを見て初期表示を切り替えたいですね。
言語設定を覚える処理も、セッションに保存しているためにその情報の破棄も速いので、一度触った言語設定は当分は覚えていて欲しいところです。
（ブラウザLocaleで初期表示の切り替えが可能なら、そうでもないかも？）

#参考資料
[PHP - laravel-5-boilerplateのルーティング - Qiita](http://qiita.com/shiichi/items/125b4464191a4565df3f)
[Laravel 5 の処理の流れ - Web Application Security Memo](http://www.pupha.net/archives/3008/)
[Laravel 5 のタマネギ構造実装処理 - Web Application Security Memo](http://www.pupha.net/archives/3032/)

Laravelのルーティング処理を理解するのにすごく役立ちました。
ありがとうございました。

#おまけコーナー
Translatorで動いているファイルローダーは、TransratorServiceProbiderで設定されたClosureです。
confing/app.phpで設定されているprovidersが読まれたタイミングで、

```php:vendor/laravel/framework/src/Illuminate/Translation/TranslationServiceProvider.php
（前略）
    public function register()
    {
        $this->registerLoader();

        $this->app->singleton('translator', function ($app) {
            $loader = $app['translation.loader'];

            // When registering the translator component, we'll need to set the default
            // locale as well as the fallback locale. So, we'll grab the application
            // configuration so we can easily get both of these values from there.
            $locale = $app['config']['app.locale'];

            $trans = new Translator($loader, $locale);

            $trans->setFallback($app['config']['app.fallback_locale']);

            return $trans;
        });
    }
（中略）
    protected function registerLoader()
    {
        $this->app->singleton('translation.loader', function ($app) {
            return new FileLoader($app['files'], $app['path.lang']);
        });
    }

（後略）
```
のregisterメソッドが動作しAppliationにtranslation.loaderとして設定され、
translatorの動作時にTranslatorのコンストラクタに渡されています。

#おまけコーナー2
途中で出てきたapp()関数ですが、分かりやすさ重視で説明をすっ飛ばしましたが、
app()関数ではContainerクラスのstaticメソッドを呼んでいます。

```php:vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
（前略）
    function app($make = null, $parameters = [])
    {
        if (is_null($make)) {
            return Container::getInstance();
        }

        return Container::getInstance()->make($make, $parameters);
    }
（後略）
```

あれ？ここで使ってるのはContainerのインスタンスなの？とちょっと迷ったんですが、
Container::getInstanceは、

```php:vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
（前略）
    public static function getInstance()
    {
        return static::$instance;
    }

（後略）
```

となっています。
そしてstatic::$instanceはsetInstanceメソッドからしか設定されません。

```php:vendor/laravel/framework/src/Illuminate/Foundation/helpers.php
（前略）
    public static function setInstance(ContainerContract $container)
    {
        static::$instance = $container;
    }
（後略）
```

さらにsetInstanceメソッドは、Containerクラスではどこからも呼ばれていません・・・！
ではどこから呼ばれているかというとこれがApplicationクラスで、コンストラクタの先頭で$thisをセットしています。
ということで、これがContainer::getInstanceしてもApplicationクラスのインスタンスが返ってくるからくりでした。