ブロックの国際化対応は大変な苦難の道のりだった!

WordPressは翻訳関数というのを用意していて、それを使えばpoファイル、moファイルによってタイトルや説明文を多国籍言語で表示させることができます。 これはWordPressを使うWeb制作者の多くが認識していらっしゃるでしょう。 もちろん私もそうでした。 でも、具体的にコーディングしたことはなく、まあその内と思っていたのです。 ひな型からimport { __ } from '@wordpress/i18n';とライブラリがインポートされているぐらいだから、手軽に使えるものと思い込み、いつでもマスターできると思っていました。 しかし、とんでもなかったです。3日かかってやっとなんとか翻訳言語が表示できるようになりましたが、とにかく苦労しました。 このノウハウを決して忘れないようにしたいと思うし、これからチャレンジする方には、少しでも苦労せず身に着けていただけたらいいという思いでブログにします。

POT、PO、MOの各ファイルの役割

まず、この図をご覧ください。

image

とりあえず、この図で大まかなイメージを掴んでおいてください。

最初の一歩はPOTファイル

まず、POTファイルです。 私は最初POTファイルなるものが何かよくわかりませんでした。いろんな解説記事を見ましたが「POファイルのテンプレート」という表現が多かったです。しかし、これって具体的にイメージしにくいのです。 そこで、私は次のように表現することにしました。 「プロジェクトのソースフォルダからプログラムファイルを検索し、その中から()_e()などの翻訳関数を抽出し、その第1引数をリスト化したファイル」 厳密さは欠けるように思いますが、どんなファイルか具体的なイメージが湧きやすい気がします。

さらに詳しく説明します。 翻訳関数は引数を2つとります。 第1引数は表示する文字列の原文です。普通は英語でしょうね。 第2引数はテキストドメインです。それってなに? 今の段階ではその説明はちょっと置いておきましょう。POファイルのところで説明します。

とにかく、この関数がプラクラム内で使用されていることが前提になります。POTファイルは、その関数の第1引数、つまり、翻訳すべき原文のリストなのです。そして、それに訳文を入力する「枠」がついていますが、POTファイルの段階では、その部分が空なのです。

なぜ空かというと、それがまさにテンプレートと言われる所以で、そのPOTをもとに日本語訳のついたファイル、中国語訳のついたファイルというように複数のPOファイルを作るためです。

ということで、POTファイルは国際化対応の根幹となるファイルだと思います。これを確実に作ることから始めるべきだと思いました。 Poeditなどの便利なアプリケーションが多くのサイトで紹介されえているのですが、このアプリケーションはPOファイルを生成するためのアプリケーションで、POTファイルは別途用意されていることが前提となっています。しかも、POTファイルなくしてプログラムのソースファイルからいきなりPOファイルを生成する機能ももっています。そのため、初心者が最初にこのアプリケーションを使うと、POTファイルの存在価値を意識しないようになってしまう気がします。これはおすすめしません。まず、POTファイルの作り方を覚えましょう。

WP-CLIのインストール

WP-CLIWordPressのよくある作業を管理するための開発者向けのコマンドラインツールです。このツールでPOTファイルを作ります。 Poeditでも作れますが、少なくともGutenbergのブロック開発環境においては、WP-CLIを使うことは必須だと思います。私は最初PoeditでPOTを生成したため、かなり遠回りをしました。PoeditはPOファイルを作るものでPOTを作るものではないように思います(有料版は試していないのでわかりません。)。

WP-CLIのインストールは次の手順で簡単にできます。

インストール手順1(SCOOPのインストール)

  1. PowerShellを管理者権限で開きます。
  2. 以下のコマンドを実行して、Scoopをインストールします:
Set-ExecutionPolicy RemoteSigned -scope CurrentUser
iex (new-object net.webclient).downloadstring('https://get.scoop.sh')

インストール手順2(WP-CLIのインストール)

  1. PowerShellを開きます。
  2. 以下のコマンドを実行して、WP-CLIをインストールします:
scoop install wp-cli

インストールが完了したら、コマンドプロンプトPowerShellでwp --infoを実行して、正しくインストールされたか確認できます。

POTファイルの生成

対象のブロックのルートディレクトリでターミナルを開いて、次のコマンドを実行します。

wp i18n make-pot ./ languages/itmar_guest_contact_block.pot --exclude=node_modules/*

第1引数./はルートディレクトリ以下のすべてのディレクトリ内のファイルを対象に関数を検索することを意味します。 第2引数は出力対象のPOTファイル名です。ファイル名は何でもよいのですが、テキストドメイン名を使うのが一般的でしょう。--excludeオプションは、検索対象の中から特定のディレクトリを除外するものです。なくてもよいのですが、ブロックの開発環境には多くの場合node_modulesディレクトリがあり、そこには大量のファイルがあるので、検索対象から外しましょう。

ということで実際に出来上がったファイルは以下のようになります。

・・・
#: guest-contact-block.php:163
msgid "Receipt processing completed successfully."
msgstr ""

#: build/index.js:132
#: src/edit.js:103
#: build/index.js:116
msgid "Inquiry information notification email"
msgstr ""
・・・

このコードはPOTファイルの一部です。msgidが原文の見出しで、msgstrが訳文の見出しです。訳文は空になっていますね。 #:以下は翻訳関数があったファイルとその行番号です。この情報が非常に重要なのです。これがないと、JSONファイルの作成のところで大きくつまづきます。

つづいてPOファイルの作成

ここでPoeditというアプリケーションを使います。 インストール方法は簡単で、次の公式ページからダウンロードしてそのファイルをダブルクリックするだけです。 https://poedit.net/download/

POファイルの作成手順はスクリーンショットで説明します。

image

image

WP-CLIで作成したPOTファイルを選択します。

image

image

①で入力項目を選択し、②で訳文を入力、③で保存です。

image

このファイル名は重要です。デフォルトでは「ja.po」となっているので、その前に「テキストドメイン-」と入れます。 ここでテキストドメインについて説明します。 テキストドメインは翻訳関数__()等の第2引数に設定すると説明しました。そうすることによって翻訳関数はそのテキストドメインの文字列を含むファイル名を持つファイルから、第1引数にセットした原文の文字列から訳文を検索するようになっているのです。つまり、

__("Notification email subject", 'itmar_guest_contact_block')

という関数があるとするとitmar_guest_contact_blockという名前を含むファイルを探し、さらに、第1引数の文字列と一致する訳文を探して表示するのです。ですから、ここでつけるファイル名は重要です。これを間違うと訳文は表示されません。

これで保存すれば無事にPOファイルは出来上がりです。

MOファイルはなんのためにある?

ではMOファイルは何のためにあるのでしょう。 先ほど翻訳関数がテキストドメイン名のついたファイルを探しにいくといいましたが、実際に探しにいくのはPOファイルではなく、MOファイルなのです。そして重要なのはこのMOファイルはPHPの翻訳関数の訳文を表示させるファイルだということです。 Javascriptの翻訳関数による訳文はMOルがあっても表示されません。

とりあえず、ここではMOファイルによる訳文の表示に絞って解説していきます。 MOファイルはPOファイルをバイナリ形式でコンパイルしたファイルで、先ほどPoeditでPOファイルを保存しましたが、そのとき自動的に生成されるようになっています。ただし、これは設定で生成されないようにもできるので、設定されているかどうかは確認しておきましょう。 Poeditを立ち上げて[ファイル]ー[設定]で次のダイアログが出るので、そこで確認できます。

image

load_plugin_textdomainによる読込

そしてさらにブロックのエントリポイントのPHPファイルに次のように記述しなくてはいけません。

function itmar_contact_block_block_init() {
    ・・・
    //PHP用のテキストドメインの読込(国際化)
    load_plugin_textdomain( 'itmar_guest_contact_block', false, basename( dirname( __FILE__ ) ) . '/languages' );
}
add_action( 'init', 'itmar_contact_block_block_init' );

WordPressのinitアクションフックでload_plugin_textdomain実行するわけです。第1引数はテキストドメイン、第3引数はMOファイルの保存フォルダへの相対パスです。今回はブロックのルートディレクトリ直下のlanguagesフォルダを指しています(第2引数はあまり気にせずfalseでいいようです。)。

これでPHPで記述された翻訳関数の部分は訳文が表示されます。 このように自分で任意のフォルダにMOファイルを保存した場合はload_plugin_textdomainで、その場所を指定する必要がありますが、.\wp-content\languages\pluginsというフォルダに保存すれば、load_plugin_textdomainでの指定は必要ありません。 ただし、このフォルダはプラグインの外にあるフォルダなのでプラグインをインストールしただけでは保存することができず、ユーザーに一手間かけさせることになります。できれば、そうしない方がいいのではないかと思います。

PHPのコメントヘッダー内の翻訳

プラグインのエントリポイントのPHPファイルにはコメントヘッダーが付いていて、これがあることでプラグイン名等が表示されます。

/**
 * Plugin Name:       Guest Contact Block
 * Plugin URI:        https://itmaroon.net
 * Description:       A block with an email submission form.
 * Requires at least: 6.1
 * Requires PHP:      7.0
 * Version:           0.1.0
 * Author:            Web Creator ITmaroon
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       itmar_guest_contact_block
 * Domain Path:             /languages

 */

こんな感じになっていますが、WP-CLIでPOTファイルを作ると、次のように抽出してくれます。

#. Plugin Name of the plugin
msgid "Guest Contact Block"
msgstr ""

#. Plugin URI of the plugin
msgid "https://itmaroon.net"
msgstr ""

#. Description of the plugin
msgid "A block with an email submission form."
msgstr ""

#. Author of the plugin
msgid "Web Creator ITmaroon"
msgstr ""

この部分については翻訳関数がセットされていなくても、POファイルに訳文を入れてMOファイルを生成するだけで翻訳されます。

JSONファイルによるJS関数の翻訳

ここまでの手順も相当複雑でしたがPOT、PO、MOの各ファイルの機能を理解していれば、そんなに苦労せずにたどり着けるのではないかと思います。 問題はここからなのです。 なかなか、正確に説明してくれている記事にも巡り合えず、ChatGPTの答えも不正確でした。

そもそも、PHPの関数とJS(JavaScript)の関数で翻訳の仕組みが違い、しかも、MOファイルではなくJSONファイルを用意しないといけないなんて思いもしませんでした。 それに気付くのにも時間がかかりました。 ブロック開発では訳文を表示させたいのは、ほとんどがJSの関数で作られています。それが表示されないと意味がありません。

それはともかく、コードとしては次のようになっています。

import { __ } from '@wordpress/i18n';
・・・
return(
    ・・・
    <TextControl
        label={__("Notification email subject", 'itmar_guest_contact_block')}
        ・・・
    />
    ・・・
)

PHPと違うのは関数のimportが必要であるという点だけです。 また、JSONファイルを作ること自体も簡単です。プラグインのルートディレクトリで次のコマンドを実行します。

wp i18n make-json languages/ --no-purge

これでプラグインのルートディレクトリ直下のlanguagesフォルダからpoファイルを探し出してjsonファイルが生成されます。 これで訳文が表示されるなら簡単なのです。 しかし、これからが苦難の始まりです。

wp_set_script_translationsによるJSONファイルの指定(失敗談)

PHPではload_plugin_textdomainでMOファイルを読み込みましたが、JSONファイルにおいてもそれと同様のプロセスが必要です。

wp_set_script_translations( $script_handle, 'itmar_guest_contact_block', plugin_dir_path( __FILE__ ) . 'languages' );

このコードをload_plugin_textdomainと同様にinitアクションフックで実行します。 第2引数がテキストドメインで、第3引数はJSONファイルが保存されているフォルダへの相対パスです。 問題は第1引数です。これはスクリプトハンドルと呼ばれる文字列です。 WordPressのテーマでもプラグインでも外部のライブラリを読み込むときはwp_enqueue_scriptというコマンドを使います。このコマンドの第1引数で指定するのがスクリプトハンドルです。wp_enqueue_scriptで指定するのは他のwp_enqueue_scriptで使用するスクリプトハンドルと重複しない任意の文字列でよいのですが、wp_set_script_translationsで使うスクリプトハンドルは、すでにwp_enqueue_script等の登録コマンドで使用されている文字列でないとダメなのです。 平たく言うと使用実績があるスクリプトハンドルということですね。それがないなら、あらかじめダミーのスクリプト用意してwp_enqueue_script等の登録コマンドを実行しておかなければいけないのです。

コードとしては次のようになります。

wp_enqueue_script(
    'itmar_script-handle',
    plugin_dir_url( __FILE__ ) .'dummy.js',
    array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-editor' ),
    '1.0.0',
    true
);

wp_set_script_translations( 
    'itmar_script-handle', 
    'itmar_guest_contact_block', plugin_dir_path( __FILE__ ) . 'languages' 
);

そんな無駄なエンキューしないといけないのかと思うのですが、これでwp_set_script_translationsは機能してくれているはずなのです。

とおもって、ブロックをリロードして表示を確認しました。 ・・・英語のままです。なぜ??? かなり、時間をかけて調べました。すると、JSONファイルのファイル名の形式は ${domain}-${locale}-${handle}.jsonまたは${domain}-${locale}-${md5}.jsonと書いてある記事を見つけました。 WP-CLIが生成したファイル名はitmar_guest_contact_block-ja-bb1d7dea005e67527e728d4801f74b61.jsonで後者の形式です。では、前者の形式にしてみようと思い、次のようにリネームしました。 itmar_guest_contact_block-ja-itmar_script-handle.json

これで再度チャレンジ! 訳文が表示されました!やったー!! リネームするのは面倒だけど、これでなんとかなるならこれでいいやと思いました。

これで他のブロックも同じように国際化対応しようと思い、POファイルを作り、WP-CLIを実行しました。 すると、さっきとは違ってJSONファイルが複数出来上っています。これってどういうことかほんとに悩みました。

調べた結果、WP-CLIはPOファイルから翻訳関数があったファイル名を読み取り、その名前をmd5ハッシュに変換してJSONのファイル名にしていました。そのため、POファイルに複数の元ファイル名が記録されていると、その数だけファイルが生成されます。 こうすることでブラウザで表示されるファイル以外の翻訳ファイルは読みこまずパフォーマンスを向上させる仕組みということもわかりました。 しかし、これをされると先のリネーム作戦は実行できません。同一フォルダに同一名のファイルは保存できないからです。 結局、フリダシに戻りました。

wp_set_script_translationsによるJSONファイルの指定(ようやく成功)

それから相当色々試してみました。po2jsonというパッケージも試しましたが、今一つしっくりきません。 その紆余曲折を語ると大変なので、最終的な結論だけ紹介します。 コードを示します。

function itmar_contact_block_block_init() {
    $script_handle = 'text_domain_handle';
    // スクリプトの登録
    wp_register_script(
        $script_handle,
        plugins_url( 'build/index.js', __FILE__ ),
        array( 'wp-blocks', 'wp-element', 'wp-i18n', 'wp-block-editor' )
    );

    //ブロックの登録
    register_block_type( __DIR__ . '/build',
        array(
            'editor_script' => $script_handle
        )
    );

    // その後、このハンドルを使用してスクリプトの翻訳をセット
    wp_set_script_translations( $script_handle, 'itmar_guest_contact_block', plugin_dir_path( __FILE__ ) . 'languages' );
    
    //PHP用のテキストドメインの読込(国際化)
    load_plugin_textdomain( 'itmar_guest_contact_block', false, basename( dirname( __FILE__ ) ) . '/languages' );
}
add_action( 'init', 'itmar_contact_block_block_init' );

このコードはこの公式ページを見て考え付きました。 やっぱり、最後は公式ページですね。 コードの解説です。 ①「// スクリプトの登録」のセクションではwp_register_scriptというコマンドを使っています。これは先に紹介したwp_enqueue_scriptと違ってスクリプトファイルをエンキューせず、スクリプトハンドルだけを登録するコマンドです。これでスクリプトハンドルを確保します。 ②「//ブロックの登録」セクションではregister_block_typeでブロックを登録しますが、その時の登録情報の一つであるeditor_scriptを①で確保したスクリプトハンドルに上書きしています。 ③「// その後、このハンドルを使用してスクリプトの翻訳をセット」のセクションでは、そのスクリプトハンドルを使って、wp_set_script_translationsを実行しているのです。

つまり、${domain}-${locale}-${md5}.jsonの形式のファイルが機能するためには、wp_set_script_translationsの第1引数は、ブロックのeditor_scriptに登録されたスクリプトハンドルである必要があるということです。editor_scriptに登録されたスクリプトハンドルというのはbuild/index.jsをロードするものでないといけません。それが上記のコードのwp_register_scriptというわけです。 @wordpress/create-blockで作ったブロックのプロジェクトではブロックの登録はblock.jsonの情報に基づいて行われるようになっています。その中では"editorScript": "file:./index.js",となっています。wp_register_scriptは、それと同等の働きをするということがわかりました。その上でスクリプトハンドルを使い回すことができるようにするというのが、今回の成功への道のりだったと言えます。

もう一点忘れていけないのはPOファイルの翻訳関数の存在していたファイル情報にbuild/index.jsが含まれていないければいけないということです。src/edit.jsだけでは表示されません。これはPOTファイルの生成に関連するもので、PoeditでPOTファイルを生成するとうまくいきませんでした。

POファイルの更新方法

最後にPOファイルの更新方法を紹介します。これはPoeditの力を借りるのが一番だと思います。 POファイルの更新というのは、ソースファイルの更新により、翻訳関数の追加、削除、内容の変更が起こったとき必要になります。 これはPOTファイルを更新する必要があるので、ソースファイルを更新したら、次のWP-CLIコマンドを実行します。

wp i18n make-pot ./ languages/itmar_guest_contact_block.pot --exclude=node_modules/*

それからPoeditを立ち上げます。

image

image

更新したいPOファイルを選択して、開いてから「POTファイルから更新...」を押します。

image

image

image

このように新しい入力枠ができています。ここに入力していくことで更新することができます。

この作業が終わってPOファイルを保存すればPoeditがMOファイルは更新してくれます。 しかし、JSONファイルは更新してくれないので、最後に次のコマンドを実行するのを忘れないで下さい。

wp i18n make-json languages/ --no-purge

長いブログになりましたが、以上にしたいと思います。 これから国際化対応をする方には、重要な情報を詰め込んだつもりです。お役に立てれば光栄です。 最後までお読みいただきありがとうございました。