Laravelで多言語用のJSONを出力するコマンドを作る
メモ PHPLaravelJavascript

Laravelで多言語用のJSONを出力するコマンドを作る

2022.05.22

こんにちはjunです。個人開発で多言語対応のLaravelアプリを作っています。多言語では各種言語の単語・文章のマッピング(辞書)をする配列・JSONを作成し、レンダリング時にメソッドを使用して文字を表示します。Laravelではresources/lang 配下にenjaのようなディレクトリを作成し、その中に言語のマッピングをします。大体は以下のような配列を作ります。

resources/lang/ja/words.php
<?php
return [
    'login'=>'ログイン',
    'logout'=>'ログアウト'
]

そしてビューファイルなどで__('words.login')のように使用します。

多言語のメソッドが__みたいな名称である理由としては、使いまくるので簡単な名称になっています。Vueとかでは$t()みたいなものを使用します。

上記のようなPHPファイルを作成してもできますが、resources/lang 直下にen.jsonja.jsonのようなJSONファイルを作成しても、多言語メソッドで呼び出せます。

JSONの弱点

JSONで作るメリットはマッピングのデータを他のアプリでも利用できることです。例えば、Vue・ReactではVuei18n、react-i18nextなどを使用します。同じように多言語メソッドを使用します。その際のマッピングデータとしてJSONを使用します。であればJSONファイルを作っておくことで、Vueや外部へマッピングデータを提供しやすくなります。

ただしJSONで作成するとLaravel側で__('words.login')のような呼び出しができません。この時JSONは以下のようになっています。

{
    "words":{
        "login":"ログイン",
        "logout":"ログアウト"
    }
}

すべて一次元にしてしまうと後で混乱してしまうので、カスケードさせておくと良いです。Vuei18nでは$t('words.login')で呼び出せますが、なぜかLaravelではJSONのカスケード配下のキーを呼び出すことができません。

そのため

  • Laravel以外のアプリへの提供→JSON
  • Laravelの多言語対応→PHP

という2重管理でそれぞれ設定しないといけなくなり、非効率的です。

解決方法

解決方法としてはPHPで作成した配列をJSONにダンプすることです。そうすることでPHPファイルとJSONの言語ファイルを作成することができます。そこで今回の記事ではPHPの言語ファイルをカスケードさせたJSONに出力するようなartisanコマンドを作ってみたいと思います。

コマンドの実装

対応する言語ディレクトリ

まずはLaravelの言語ファイルのドキュメントの通り、resources/lang 配下にenjaのようなディレクトリを作成しておきましょう。今回は英語と日本語にしておきます。

resources
├── lang
│   ├── en
│   │   ├── auth.php
│   │   ├── words.php
│   │   └── exceptions.php
│   └── ja
│       ├── auth.php
│       ├── words.php
│       └── exceptions.php
...

そしてそのディレクトリごとにphpファイルを分けます。とりあえずこのようにしておきます。

コマンドのファイルを作成

それではカスタムのコマンドを作成しましょう。

php artisan make:command Dumplang

コマンドもartisanで作成できます。app/Console/Commandsというディレクトリが作成され、そこにファイルが作成されます。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class Dumblang extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'lang:dump';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Convert each php lang files to JSON files.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        
    }
}

まずは上記のように、シグネチャと説明を記述します。シグネチャではlang:dump とすると

php artisan lang:dump

と入力するとこのコマンドを実行できます。

言語ディレクトリからファイルを読み込む

コマンドの内容はhandle()に記述します。全体は以下の通りです。

use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Lang;

public function handle()
{
    $suports = ["ja","en"];
    
    foreach($suports as $lng){
        $langdir = resource_path('lang/'.$lng);
        if(is_dir($langdir)){
            $files = scandir($langdir);
            if($files === false) continue;

            $files = array_filter($files,function($f){
                return strpos($f,'.php') !== false;
            });

            $trans = [];
            foreach($files as $f){
                $content_key = str_replace('.php','',$f);
                $content = include resource_path("lang/$lng/$f");
                $trans[$content_key] = $content;
            }

            $json = json_encode($trans,JSON_UNESCAPED_UNICODE);
            File::put(resource_path('lang/'.$lng.'.json'),$json);
            File::put(base_path('nuxt/lang/'.$lng.'.json'),$json);
        }else{
            print("Lang directory for ${$lng} dose not exisits");
        }
    }
    return Command::SUCCESS;
}
$suports = ["ja","en"];

foreach($suports as $lng){

}

まずは取得予定の言語の配列を用意します。それを再帰的に処理します。

元となる言語ディレクトリからファイルの一覧を取得します。

foreach($suports as $lng){
    $langdir = resource_path('lang/'.$lng);
    if(is_dir($langdir)){
        $files = scandir($langdir);
        if($files === false) continue;

        $files = array_filter($files,function($f){
            return strpos($f,'.php') !== false;
        });

        $trans = [];
        foreach($files as $f){
            $content_key = str_replace('.php','',$f);
            $content = include resource_path("lang/$lng/$f");
            $trans[$content_key] = $content;
        }

        // ....
    }
}

resource_path('lang/'.$lng)で先程の言語ディレクトリのパスを取得し、scandir()を用いて内部のファイルを配列で取得します。scandir()はphp以外のファイルや.みたいなSELinuxが勝手に作る謎ファイルも読み取ってしまうので、array_filter()を用いてフィルターします。

一つの配列に打ち込んでJSON化する

$trans = [];
foreach($files as $f){
    $content_key = str_replace('.php','',$f);
    $content = include resource_path("lang/$lng/$f");
    $trans[$content_key] = $content;
}

JSONではphpファイル名を一次キーとして利用したいので str_replace('.php','',$f)でファイル名を取得します。include resource_path("lang/$lng/$f")でphpファイルの記述を取得ます。そしてJSONにする配列に、ファイル名をキーとして打ち込みます。$trans[$content_key] = $content; それをスキャンした言語PHPファイル全てに行います。

ファイルを出力

$json = json_encode($trans,JSON_UNESCAPED_UNICODE);
File::put(resource_path('lang/'.$lng.'.json'),$json);

そして1つにまとめた配列をjson_encodeをしておき、それをresources/lang 配下します。その際には言語名.jsonとなるようにしておきます。

全容と実行

コードは以下の通りです。

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Lang;

class Dumblang extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'lang:dump';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Convert each php lang files to JSON files.';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $suports = config('app.support_langs');
        
        foreach($suports as $lng){
            $langdir = resource_path('lang/'.$lng);
            if(is_dir($langdir)){
                $files = scandir($langdir);
                if($files === false) continue;

                $files = array_filter($files,function($f){
                    return strpos($f,'.php') !== false;
                });

                $trans = [];
                foreach($files as $f){
                    $content_key = str_replace('.php','',$f);
                    $content = include resource_path("lang/$lng/$f");
                    $trans[$content_key] = $content;
                }

                $json = json_encode($trans,JSON_UNESCAPED_UNICODE);
                File::put(resource_path('lang/'.$lng.'.json'),$json);
            }else{
                print("Lang directory for ${$lng} dose not exisits");
            }
        }
        return Command::SUCCESS;
    }
}

ちょっと気をつける点としては

  • is_dir($langdir) で言語ディレクトリの存在チェック
  • scandir() で取得したファイルをフィルタする

実行してみるとja.jsonen.jsonというのが作成され、みてみると

resources
├── lang
│   ├── en
│   │   ├── auth.php
│   │   ├── words.php
│   │   └── exceptions.php
│   └── ja
│       ├── auth.php
│       ├── words.php
│       └── exceptions.php
...

ja.json
{
    "auth":{
        "login":"ログイン",
        "logout":"ログアウト",
    },
    "words":{
        "save":"保存",
        "update":"更新"
    },
    "exceptions":{
        "401":"ログインしてください。",
        "model":{
            "403":"このデータにアクセスできません。",
            "404":"対応するデータが見つかりません。"
        }
    }
}
en.json
{
    "auth":{
        "login":"Login",
        "logout":"Logout",
    },
    "words":{
        "save":"Saving",
        "update":"Updating"
    },
    "exceptions":{
        "401":"Please login.",
        "model":{
            "403":"You can not access to this data.",
            "404":"The data you request is not found."
        }
    }
}

これでPHPファイルだけでJSONの言語ファイルも作成して、フロントでのVueの言語ファイルなどに提供することができるようになります。動的にこのJSONファイルは生成するので、バージョン管理をするときは.gitignoreで指定しておくといいです。そしてデプロイや更新時にはこのコマンドを打ち忘れないようにしましょう。

ちなみにですが、実際にやっていることは特定のディレクトリのPHPファイルの内容を取得して、それをJSONにして生成したファイルを置いているだけなので素のPHP、pythonやrubyとか他の言語でも行けると思いますよ。

Copyright © 2021 jun. All rights reserved.