Laravelで多言語サイトのベースを作る。言語切替え&Cookie保存&後から言語追加可能に

Laravel 多言語サイトのベースを作る Laravel
※当サイトはアフィリエイト広告を掲載しています。

Laravelで多言語サイト構築案件のお声がかかりました。私はLaravelでの多言語サイトは初めてですが、フレームワークの機能としてあるので安心して取り組めることは認識しています。

とは言え初めてということもあり、事前に多言語化サイトのルーティングや、言語切り替え機能の実装くらいは試しておこう……。

ということで、多言語化サイトのベース部分を考えてみることにしました。同じような状況にある方の参考になれば幸いです。

本ページは、私の試行錯誤や考え・経過・失敗等をまとめたもので、ベストプラクティスではありません。あくまで多言語化サイトを作る上での経験を共有できればと思い書いています。

Lara
Lara

もしも間違いや改善点などあれば是非ご教示ください!

やりたいこと

実装する前に、開発したい内容を考えてみたいと思います。

URL構造

よくあるのが、こんな感じでしょうか。

  • https://example.jp/ja →日本語
  • https://example.jp/en →英語

などといったように、プレフィックスをつけて各言語のサイトとしようと思います。

言語の優先順位

言語を判別するには、以下の優先順位で行うものとします。

  1. URL:
    https://example.jp/ja などのURLでの指定
  2. Cookie:
    Cookieに言語が保存してあればそれを使う
  3. Accept-Language:
    Accept-Languageリクエストヘッダーの指定
  4. サイトのデフォルト言語:
    何も指定がなければアプリケーションがデフォルトに指定する言語を使う

私はCookieにしましたが、Sessionを使う手もあると思います。

トップページ

多言語前提のため、トップページ、つまりhttps://example.jp/のようなURLは使いません。

アクセスされた際は、先ほどの「言語の優先順位」の2以降の観点でリダイレクトします。

言語の変更

リストから言語を変更すると、同一ページの内容でURLと言語が変更されます。これもまあよくあるやつですね。

言語の変更=明示的に言語を選択したことになるので、この内容はCookieに保存します。

Lara
Lara

これらの仕様は、実は作りながら考えたものです。本来は仕様を最初に考えておいた方が良いですね。

インストール

すぐに開発に移れる状態まで、インストールしておきます。

前提

以下などは当然必要になりますが、すでにインストール済みとして進めます。

  • PHP
  • Composer
  • Node.js

Laravelのインストール

今回はLaravelを使うため、こちらもインストールを済ませておきます。このあたりは以下のページもご参考ください。

※後述しますがタイムゾーンの設定やロケールは変更の必要はありません。

本ページではLaravel10を使いますが、基礎的な内容です。そのため他のバージョンでも共通することが多いと思います。

タイムゾーン

まずは設定で重要になるのは、タイムゾーンです。

条件反射でconfig/app.phpのタイムゾーンを日本時間に変更してしまうところですが、多言語サイトでは考える必要がありそうです。

UTC(協定世界時)を使う

多言語サイトは、世界中のさまざまな人が利用する可能性があります。そのため、サーバーのタイムゾーンとユーザーのローカルタイムゾーンとの間でズレが生じると問題です。

であればLaravelのタイムゾーンはどうすればよいかとなりますが、UTC(協定世界時)を使うのが良さそうに思いました。

ですので設定はデフォルトの以下のままにしておきます。

'timezone' => 'UTC',

時間の利用時は?

保存はUTCにしますが、時間を表示する際は、ユーザーのタイムゾーンに変更することも視野に入れたいと思います。

タイムゾーンはどうすれば取得できるか調べたところ、以下の様な方法があるようです。

  • ユーザーがアカウント設定で明示的にタイムゾーンを選択する。
  • ブラウザのJavaScriptを使用して自動的に検出する。
  • ユーザーのIPアドレスから推定する。

余談ですが、言語(en等)からタイムゾーンを取得してはいけません。日本に住むアメリカ人のような存在は普通にあるからです。

ロケール

ロケールもついつい日本に設定するのが癖になっているところですが、考える必要があります。

そもそもロケールとは!?

途中、「ロケール」と「言語」がこんがらがりました(汗)。

ロケール=国や地域ですので、厳密に言えば言語とは異なります。しかしLaravelのロケール設定に関して言えば、ほぼ言語と同じと思ってさしつかえ無さそうです。

実際はロケールごとに検討すべき事(日付の表示を変えたり等)がありますが、それは自分で組み込むべきことだからです。

Laravelの設定の側面としては、(少なくとも私が使用する範囲では)ロケール=言語設定となりそうです。

そのせいでファイル名・設定値・変数名をlanguageにしようかlocaleにしようか迷う事になりました。また、本ページでも混在しているところもありますが、同一視してお読みください。

ロケール設定

タイムゾーン設定はUTCベースで固定しますが、ロケール設定は動的に変更することになります。

ですので初期設定も変更されていくため、私はとりあえずそのままにしました。もちろんここでデフォルトのロケールとして設定するのもありです。

'locale' => 'en',
'fallback_locale' => 'en',

ロケール変更時

ロケール変更時は、以下の様にsetLocaleメソッドを使います。これで動的にアプリケーションのロケールを変更可能です。

 App::setLocale($locale);

システムで許容するロケール

システムで許容するロケールを定義しておきます。想定外のロケールに変更できるようなシステムはザルですからね。

config\languages.phpを作り、以下のように定義します。テスト用なので2ヵ国語でためしますが、後からいくらでも追加可能です。

<?php

$supportedLanguages = [
    'en' => 'english',
    'ja' => '日本語',
]; 

return [
    'supported' => $supportedLanguages,
    'default' => array_key_first($supportedLanguages),
];

ご覧のとおり、連想配列で指定しています。'default'はとりあえずあった方が分かりやすいかと思い作りましたが、無くても良かったかもしれません。

'default'を指定しない場合でも、最初に記載した言語が通常はデフォルトになります(後述)。

翻訳

以下のコマンドで、langディレクトリを初期化します。

php artisan lang:publish

Laravel8以前はresources/lang/に、Laravel9以降は、最上位にlangディレクトリが作成されます。以後langディレクトリとします。

コマンドを発行することで、デフォルトロケールである今回はlang/en/に、複数のファイルが作成されています。その中にあるlang/en/message.phpを編集します。

<?php
return [
    'welcome' => 'Welcome to our first multilingual site!',
];

次に、lang/ja/message.phpに、同様の感じで以下とします。

<?php
return [
    'welcome' => '初めての多言語サイトへようこそ!',
];

これで、英語と日本語のメッセージが作成できました。

余談ですが、ここで、冒頭の<?phpを入れ忘れたために表示時エラーになり、ちょっと時間を使ってしまいました(汗)

ルート

routes/web.phpを以下の様に設定します。

<?php

use App\Http\Controllers\WebController;
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    //ミドルウェアでなんとかしてくれる
});

// 各言語の下層(例: /ja 以下など実質の公開サイト)
Route::group(['prefix' => '{locale}', 'where' => ['locale' => '[a-z]{2}']], function () {
    Route::get('/', [WebController::class, 'index'])->name('web.index');
    Route::post('/set-language', [WebController::class, 'setLanguage'])->name('web.change-language');
});

機能するのは計3ルートです。

  1. トップ(/):
    ミドルウェアで各言語にリダイレクトするので、実質使用しません。
  2. 言語トップ(/ja):
    各言語のトップページです。簡単な動作確認だけなのでこれだけで。12行目で言語部分を取得しています。
  3. 言語変更(/ja/set-language):
    言語を切り替えるために使用します。ページ表示は無く、Cookieセット後にリダイレクトします。ここだけPOSTにしました。

ミドルウェア

ミドルウェアでロケール関連の処理を行います。これにより、コントローラーに処理が移る前にロケールが決まるため、多言語をあまり意識しなくて済みそうです。

まずはミドルウェアのファイルを作成します。

php artisan make:middleware SetLocale

そして以下の様に修正します。

<?php

namespace App\Http\Middleware;

use Closure;
use App;

class SetLocale
{
    public function handle($request, Closure $next)
    {
        $supportedLanguages = array_keys(config('languages.supported'));
        $defaultLanguage = config('languages.default');
        $routeLocale = $request->route('locale');

        if ($routeLocale && !in_array($routeLocale, $supportedLanguages)) {
             // サポートされていない言語コード
            abort(404);
        }

        $cookieLocale = $request->cookie('app_locale');
        if ($cookieLocale && !in_array($cookieLocale, $supportedLanguages)) {
            // 不正なクッキーは無視
            $cookieLocale = '';
        }

        // topページへのリクエスト
        if ($request->path() == '/') {
            if ($cookieLocale) {
                $locale = $cookieLocale;
            } else {
                $locale = $this->getDefaultLanguage($supportedLanguages, $defaultLanguage);
            }
            return redirect('/' . $locale);
        }

        $locale = $routeLocale ?: $this->getDefaultLanguage($supportedLanguages, $defaultLanguage);
        App::setLocale($locale);

        return $next($request);
    }

    private function getDefaultLanguage($supportedLanguages, $defaultLanguage)
    {
        $language = request()->getPreferredLanguage($supportedLanguages);
        if (!$language) {
            $language = $defaultLanguage;
        }
        return $language;
    }
}

Webは全てこのミドルウェアを通すため、言語コードのバリデーションも兼ねています。

コメント多めで書いていますので、何となくやっていることは分かると思います。

不正な言語コードがリクエストされる心配を、コントローラー以降で考えなくても良いようにしています。

Kernel.phpに追加

ミドルウェアを書いただけでは適用されません。

app\Http\Kernel.phpの$middlewareGroupsの'web'内に追記します。10行目を参照ください。

// 中略
protected $middlewareGroups = [
        'web' => [
            \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,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\SetLocale::class,
        ],
// 中略

とりあえずAPIリクエスト時のことは考えません。$middlewareに入れればとは思うのですが、そこだとルートのロケールが取得できないためです。良い方法があれば追記します。

コントローラー

以下のコマンドでコントローラーを作成します。

make:controller WebController

そのファイルapp\Http\Controllers\WebController.phpを以下の様に修正します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\View\View;

class WebController extends Controller
{
    // 言語トップ
    public function index(Request $request): View
    {
        return view('index', [
            'locale' => app()->getLocale(),
            'cookieLocale' => $request->cookie('app_locale'),
            'preferredLocale' => request()->getPreferredLanguage(),
        ]);
    }

    // 言語セット
    public function setLanguage(Request $request, $locale)
    {
        // テストのため短めに30秒とする
        $cookie = cookie('app_locale', $locale, 30);

        // 直前のページ(実際はバリデーションして使う)
        $previousUrl = $request->input('currentPath', '/');
        $updatedUrl = preg_replace('/\/[a-z]{2}(\/|$)/', "/{$locale}", $previousUrl);

        return redirect($updatedUrl)->withCookie($cookie);       
    }
}

ビュー

resources\views\index.blade.phpを作成&編集します。これは言語のトップページになりますが、デバッグ用に各種言語設定を出力しています。

言語を変更する部分はサイト全体で使うはずなので、コンポーネントにしました。

<h1>{{ __('messages.welcome') }}</h1>

<x-language-selector />

<br>
appLocale:
{{ $locale }}
<br>
cookieLocale:
{{ $cookieLocale }}
<br>
preferredLocale
{{ $preferredLocale }}

コンポーネント

以下のコマンドで、コンポーネントを作ります。

php artisan make:component LanguageSelector

app\View\Components\LanguageSelector.phpが出来るので、以下の様に編集します。

<?php

namespace App\View\Components;

use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

class LanguageSelector extends Component
{
    public $languages;
    public $currentLocale;

    /**
     * Create a new component instance.
     */
    public function __construct()
    {
        $this->languages = config('languages.supported');
        $this->currentLocale = app()->getLocale();
    }

    /**
     * Get the view / contents that represent the component.
     */
    public function render(): View|Closure|string
    {
        return view('components.language-selector');
    }
}

resources\views\components\language-selector.blade.phpも出来ているので、以下の様に編集します。

<form id="js-languageForm" method="POST" action="">
    @csrf
    <input type="hidden" name="currentPath" value="/{{ request()->path() }}">
    <select name="language" onchange="updateActionAndSubmit(this);">
        @foreach ($languages as $code => $name)
            <option value="{{ $code }}" {{ $currentLocale == $code ? 'selected' : '' }}>
                {{ $name }}
            </option>
        @endforeach
    </select>
</form>

<script>
    function updateActionAndSubmit(select) {
        var selectedLanguage = select.value;
        var form = document.getElementById('js-languageForm');
        form.action = '/' + selectedLanguage + '/set-language';
        form.submit();
    }
</script>

これで、変更があったときに/en/set-languageなどに飛び、Cookieをセット&リダイレクトします。

動作確認

ルートページにアクセス

/でアクセスすると、私のブラウザは日本語環境なので、その設定が優先されます。結果、ミドルウェアにより/jaに転送されました。

一緒にロケールを表示させています。上から、以下を意味します。

  • appLocale: Laravelにセットしたロケール
  • cookieLocale: Cookieに保存されているロケール
  • preferredLocale: ブラウザのロケール

英語に変更

言語をenglishにしてみました。Cookieに保存されていることが確認できます。

再度Topへアクセス

再度Topの/へアクセスすると、Cookieを読み取り優先して、先ほどと同じ英語ページが表示されます。

日本語URL直打ち

/jaなどのURLを直打ちすると、きちんと表示されます。これはURL(パス)が第1の優先順位になっているからであり、仕様通りです。

存在しないページへアクセス

/aaなどの許容しない言語を選択すると、ミドルウェアがサポートされていない言語と判別し、404を返します。

まとめ

以上により、多言語サイトのベースができました。後からいくらでも言語は追加できますし、ビューで__()を使うということ以外は、それほど言語を意識しないで済みます。

他に多言語で懸念するのは、時間でしょうか。例えば通販などで考えると、注文時間などは現地時間で表示されたいところです。UTCで保存していますので、出力時に現地時間に変換するなどが必要です。

逆に言えば、そのあたりをクリアできれば、多言語サイトもなんとかなりそうです。

初めて業務として請ける際も、大丈夫そうです。Laravel様々……と言えますね。

コメント

タイトルとURLをコピーしました