1. ホーム
  2. WordPress

【WordPress】カスタムブロックを翻訳対応させる方法|@wordpress/i18n の __() と wp_set_script_translations

Share

カスタムブロックを作ったとき、ブロック挿入メニューに出る名前や設定サイドバーのラベルを英語で書いたままにしていませんか。PHP のテンプレートなら __() で囲んで翻訳ファイルを用意すれば日本語になりますが、ブロックの JavaScript の文字列は同じ翻訳ファイルでは読み込まれません。JS 用に JSON へ変換したファイルを、wp_set_script_translations() で明示的にひも付ける必要があります。この記事では @wordpress/i18n の関数の使い方から、テキストドメインの決め方、翻訳ファイルの作成コマンドまで、ブロックを翻訳対応させる一連の流れを解説します。

JS の文字列は PHP の翻訳ファイルでは訳されない

WordPress の翻訳は gettext という仕組みを使っています。ソースコードから翻訳対象の文字列を抜き出して .pot というひな形を作り、言語ごとに訳を入れた .po を用意し、それを機械が読む形式の .mo に変換して読み込む、という流れです。PHP 側はこの .mo を読みます。

ところがブロックエディターの UI はブラウザー上の JavaScript が組み立てています。ブラウザーは .mo というバイナリファイルを直接読めないため、WordPress 5.0 以降はJS の翻訳だけ JSON 形式で配信する仕組みが用意されました。つまり、ブロックを翻訳対応させるには次の2つが必要になります。

  • JS のソースコード側で、文字列を @wordpress/i18n の関数で囲む
  • PHP 側で、その JS ファイルに翻訳 JSON をひも付ける(wp_set_script_translations())

どちらか片方だけでは動きません。「__() で囲んだのに英語のまま」というトラブルのほとんどは、2つめの手順が抜けているか、JSON を作っていないことが原因です。

@wordpress/i18n の翻訳関数

JS 側で使う関数は @wordpress/i18n パッケージから読み込みます。名前も引数の並びも PHP 版とほぼ同じで、PHP で __() や _n() を書いたことがあればそのまま使えます。

関数引数説明
__()( text, domain )いちばん基本の関数。文字列を訳して返す
_x()( text, context, domain )翻訳者向けの文脈(context)を添える。同じ単語でも訳し分けが必要なときに使う
_n()( single, plural, number, domain )number の値に応じて単数形・複数形を選んで訳す
_nx()( single, plural, number, context, domain )_n() に文脈を足したもの
sprintf()( format, ...args )翻訳済みの文字列に値を埋め込む。翻訳関数ではないが、@wordpress/i18n から一緒に提供される
isRTL()なし現在のロケールが右から左に書く言語かどうかを真偽値で返す

共通しているのは、テキストドメインが必ず最後の引数だという点です。_x() なら第3引数、_n() なら第4引数になります。ここを間違えると翻訳が見つからず、原文がそのまま表示されます。

edit.js での書き方

実際のブロックに当てはめてみます。設定サイドバーのパネル名やコントロールのラベル、プレースホルダーなど、画面に出る文字列はすべて関数で囲むのが基本です。

src/edit.js
import { __, _x, _n, sprintf } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
    const { heading, itemCount } = attributes;
    const blockProps = useBlockProps();

    return (
        <>
            <InspectorControls>
                <PanelBody title={ __( 'Card settings', 'my-blocks' ) }>
                    <TextControl
                        __nextHasNoMarginBottom
                        label={ __( 'Heading', 'my-blocks' ) }
                        help={ __( 'Shown at the top of the card.', 'my-blocks' ) }
                        value={ heading }
                        onChange={ ( value ) =>
                            setAttributes( { heading: value } )
                        }
                    />
                </PanelBody>
            </InspectorControls>

            <div { ...blockProps }>
                <h3>
                    { heading || _x( 'Card', 'block placeholder', 'my-blocks' ) }
                </h3>
                <p>
                    { sprintf(
                        /* translators: %d: 表示中の項目数 */
                        _n( '%d item', '%d items', itemCount, 'my-blocks' ),
                        itemCount
                    ) }
                </p>
            </div>
        </>
    );
}

ポイントは _n() と sprintf() の組み合わせです。_n() は数を渡すと単数形か複数形を選んで訳文を返すだけで、%d の部分は置き換えてくれません。実際の数字を入れるには、返ってきた文字列を sprintf() に渡します。日本語には単複の区別がありませんが、翻訳先の言語によっては3つ以上の形を持つものもあるため、数を伴う文言は最初から _n() で書いておくのが親切です。

その直前に書いた /* translators: */ で始まるコメントは、翻訳者へのメモです。抽出ツールがこのコメントを拾って翻訳ファイルに書き出してくれるので、%s や %d が何を指すのかを一言添えておくと、訳す人が文脈を取り違えずに済みます。関数呼び出しの直前の行に書くのが決まりです。

_x() の文脈も同じ発想です。たとえば英語の “Card” は「カード(名詞)」とも読めますし、文脈によっては別の訳が適切な場合もあります。'block placeholder' のような短い説明を添えておくと、翻訳ファイル上で別々の項目として扱われ、それぞれに適切な訳を当てられます。

テキストドメインを決めて block.json に書く

テキストドメインは「この文字列はどのプラグイン(テーマ)のものか」を示す識別子です。WordPress 全体で1つの翻訳の入れ物を共有しているため、これがないと他のプラグインの訳と混ざってしまいます。

決め方は簡単で、プラグインのスラッグ(ディレクトリ名)と同じ文字列にします。WordPress.org で配布する場合はスラッグと一致していることが必須で、一致していないと翻訳が読み込まれません。小文字とハイフンだけで書き、アンダースコアは使いません。値は3か所に書きます。

my-blocks.php(プラグインヘッダー)
<?php
/**
 * Plugin Name: My Blocks
 * Description: カスタムブロックのサンプル。
 * Version:     1.0.0
 * Text Domain: my-blocks
 * Domain Path: /languages
 */

Domain Path は、プラグインが自前で翻訳ファイルを同梱する場合にその置き場所を示すヘッダーです。次に block.json です。

src/card/block.json
{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "my-blocks/card",
  "title": "Card",
  "description": "A card block with a heading.",
  "keywords": [ "card", "box" ],
  "category": "design",
  "textdomain": "my-blocks",
  "editorScript": "file:./index.js",
  "style": "file:./style-index.css"
}

textdomain フィールドには2つの役割があります。ひとつは block.json 自体に書いた文言を翻訳対象として登録すること。もうひとつは、後述する JS 翻訳の自動ひも付けです。翻訳される block.json のフィールドは決まっていて、次のとおりです。

フィールド内容
titleブロック挿入メニューに出るブロック名
descriptionブロックの説明文
keywords検索用のキーワード(配列のすべての要素)
styles[].labelブロックスタイルの表示名
variations[].title / description / keywordsブロックバリエーションの名前・説明・キーワード

これらは register_block_type() が読み込むときに PHP 側で訳されます。つまり block.json の文言は PHP の翻訳ファイル(.mo)、edit.js の文言は JS 用の JSON、という別ルートになっています。「ブロック名は日本語になったのにサイドバーは英語のまま」という状態は、JSON 側だけが用意できていないことを意味します。

wp_set_script_translations() で JS に翻訳を届ける

ここが本題です。wp_set_script_translations() は、登録済みのスクリプトに対して「このスクリプトはこのテキストドメインの翻訳を使う」と宣言する関数です。シグネチャは次のとおりです。

wp_set_script_translations() のシグネチャ
wp_set_script_translations(
    string $handle,           // 対象のスクリプトハンドル
    string $domain = 'default', // テキストドメイン
    string $path = ''         // 翻訳ファイルを置いたディレクトリの絶対パス
): bool

第3引数の $path は、プラグインに翻訳ファイルを同梱する場合に指定します。省略すると WordPress の言語ディレクトリ(wp-content/languages/plugins/ など)だけを探します。WordPress.org 経由で配布し、translate.wordpress.org の翻訳を使うならそこに配置されるので省略で構いませんが、自前の languages/ フォルダーに置くなら第3引数が必須です。

重要な注意として、この関数はスクリプトがすでに登録済みでないと効きません。ブロックの場合は register_block_type() がスクリプトの登録も行うので、必ずそのあとに呼びます。

ブロックのスクリプトハンドル名の決まり

block.json でスクリプトを指定した場合、ハンドル名は WordPress が自動生成します。ルールは「ブロック名の / をハイフンに置き換えたもの+フィールドに応じた接尾辞」です。

block.json のフィールド接尾辞my-blocks/card の場合
editorScript-editor-scriptmy-blocks-card-editor-script
script-scriptmy-blocks-card-script
viewScript-view-scriptmy-blocks-card-view-script
editorStyle-editor-stylemy-blocks-card-editor-style
style-stylemy-blocks-card-style

この名前を手で組み立てる必要はありません。generate_block_asset_handle( $block_name, $field_name ) という関数が用意されているので、これを使えばブロック名を変えたときの直し忘れも防げます。1つのフィールドに複数のファイルを配列で指定した場合は、2つめ以降に -2、-3 という番号が付くので、第3引数にインデックスを渡して取得します。

my-blocks.php
function my_blocks_register_blocks() {
    // 1. ブロックを登録する(このときスクリプトも登録される)
    register_block_type( __DIR__ . '/build/card' );

    // 2. 登録済みのハンドルに翻訳をひも付ける
    wp_set_script_translations(
        generate_block_asset_handle( 'my-blocks/card', 'editorScript' ),
        'my-blocks',
        plugin_dir_path( __FILE__ ) . 'languages'
    );
}
add_action( 'init', 'my_blocks_register_blocks' );

block.json に textdomain があれば自動でも呼ばれる

実は WordPress は、block.json に textdomain が書かれていて、かつそのスクリプトの依存関係に wp-i18n が含まれている場合、wp_set_script_translations() を自動的に呼んでくれます。@wordpress/scripts でビルドすると、@wordpress/i18n を import したファイルには自動で wp-i18n が依存として記録される(index.asset.php に出力される)ので、この条件は自然に満たされます。

ただし、このとき自動で渡されるのはハンドル名とテキストドメインの2つだけで、パスは渡されません。WordPress の言語ディレクトリしか探さないため、プラグインの languages/ に翻訳を同梱する構成では見つけてもらえません。同梱する場合は、上のコードのように自分で呼び直してください。第3引数付きで呼び直せば、そちらの指定が使われます。

翻訳ファイルを作る(POT → PO → JSON)

コードの準備ができたら、翻訳ファイルを用意します。ここでは WP-CLI を使います。プラグインのルートで実行してください。

ターミナル
# 1. 翻訳の置き場所を作る
mkdir languages

# 2. ソースから文字列を抽出して POT(ひな形)を作る
wp i18n make-pot ./ languages/my-blocks.pot

# 3. 言語ごとにコピーして訳を書き込む(日本語なら ja)
cp languages/my-blocks.pot languages/my-blocks-ja.po

# 4. PHP 用の MO を作る
wp i18n make-mo languages

# 5. JS 用の JSON を作る(これが JS 翻訳の本体)
wp i18n make-json languages

wp i18n make-pot <source> [<destination>] は PHP と JavaScript の両方、さらに block.json の翻訳対象フィールドまでまとめて走査してくれます。テキストドメインはプラグインヘッダーの Text Domain から自動で読み取られますが、明示したいときは --domain=my-blocks を付けます。

3番目の手順で作った .po の編集には Poedit のような専用エディターを使うと楽です。ファイル冒頭のヘッダーに "Language: ja\n" の行が必要なので、テキストエディターで直接書く場合は忘れないでください。

そして最後の wp i18n make-json <source> [<destination>] が今回のキモです。PO ファイルの中からJavaScript 由来の文字列だけを抜き出し、JS ファイル1つにつき1つの JSON を書き出します。ここまでやって初めて、エディター上の文言が翻訳されます。

ディレクトリ構成の例
my-blocks/
├── my-blocks.php
├── build/
│   └── card/
│       ├── block.json
│       ├── index.js
│       └── index.asset.php
├── src/
│   └── card/
│       ├── block.json
│       └── edit.js
└── languages/
    ├── my-blocks.pot
    ├── my-blocks-ja.po
    ├── my-blocks-ja.mo
    └── my-blocks-ja-xxxxxxxx.json

JSON のファイル名には決まりがある

WordPress が探しに行くファイル名は2種類あります。{テキストドメイン}-{ロケール}-{スクリプトハンドル}.json と、{テキストドメイン}-{ロケール}-{JS ファイルの相対パスの MD5}.json です。前者が先に試され、無ければ後者が試されます。

wp i18n make-json が出力するのは MD5 のほうです。そのまま置けば読み込まれる想定ですが、MD5 の元になるのは実際に読み込まれる JS ファイルのパスなので、抽出元(src/)とビルド後(build/)でパスが変わる構成では一致しないことがあります。うまく読み込まれないときは、ファイル名をハンドル名の形に変えてみてください。上の例なら my-blocks-ja-my-blocks-card-editor-script.json です。この名前ならパスに依存しないので確実です。

翻訳が反映されないとき

JS の翻訳は、どこか1か所ずれているだけで静かに原文へフォールバックします。エラーも警告も出ないので、原因を切り分ける順番を決めておくと早く直せます。

JSON を作っていない(PO や MO のままになっている)

いちばん多い原因です。.po に訳を書いて .mo まで作ると、PHP 側の文言は日本語になります。そのため「翻訳ファイルは用意できている」と思い込みやすいのですが、JS はその .mo を読みません。languages/ の中に .json があるかどうかをまず確認してください。無ければ wp i18n make-json languages を実行します。

また、翻訳を追加したあとに make-json を実行し忘れるパターンもよくあります。.po を更新したら JSON も作り直す、というのをセットで覚えておきましょう。

wp_set_script_translations() を呼んでいない、または呼ぶ順番が逆

この関数は「スクリプトがすでに登録されていること」が前提です。register_block_type() より前に呼ぶと、対象のハンドルがまだ存在しないため何も起きません。init フックの中で、必ず登録のあとに書いてください。

プラグインの languages/ に翻訳を同梱しているのに第3引数を省略している場合も同じ症状になります。block.json の textdomain による自動呼び出しはパスを渡さないため、同梱ファイルは見つけてもらえません。plugin_dir_path( __FILE__ ) . 'languages' のように絶対パスで渡します(URL ではありません)。

テキストドメインが場所によって食い違っている

プラグインヘッダーの Text Domain、block.json の textdomain、JS の __( '...', 'my-blocks' ) の第2引数、wp_set_script_translations() の第2引数。この4か所が完全に同じ文字列でなければ翻訳はつながりません。my_blocks と my-blocks、myblocks といった揺れが典型です。

また、JSON のファイル名の先頭もテキストドメインです。my-blocks-ja-....json になっているかを見れば、生成時のドメインが合っているか確認できます。合っていなければ make-json に --domain=my-blocks を付けて作り直してください。

__() の引数に変数を渡していて抽出されない

翻訳対象の抽出は、コードを実行せずにソースを読んで行われます。そのため、次のような書き方では文字列を見つけられず、POT に含まれません。実行時には __() が「登録されていない文字列」を受け取ることになり、原文がそのまま返ります。

src/edit.js(抽出されない書き方)
// NG: 変数を渡している
const label = 'Card settings';
__( label, 'my-blocks' );

// NG: 文字列を連結している
__( 'Card ' + type, 'my-blocks' );

// NG: テンプレートリテラルで値を埋め込んでいる
__( `Showing ${ count } items`, 'my-blocks' );

// OK: 値の埋め込みは sprintf に任せる
sprintf(
    /* translators: %d: 表示中の項目数 */
    __( 'Showing %d items', 'my-blocks' ),
    count
);

翻訳関数に渡してよいのはそのまま書かれた文字列リテラルだけと覚えてください。値を差し込みたいときは %s(文字列)や %d(数値)のプレースホルダーを使い、sprintf() で埋めます。複数の値を入れる場合は %1$s・%2$s のように番号付きにしておくと、語順が違う言語でも訳せるようになります。

なお、テキストドメイン自体も文字列リテラルで書く必要があります。定数にまとめたくなりますが、抽出できなくなるので避けてください。

スクリプトハンドル名が違っている

手書きでハンドル名を指定していて、block.json のブロック名を変えたあとに直し忘れる、という事故です。wp_set_script_translations() は存在しないハンドルを渡されても例外を投げず false を返すだけなので、気づきにくいのが厄介なところです。

先に紹介した generate_block_asset_handle() を使えば、この種のずれは起きません。手で書く場合は「ブロック名のスラッシュをハイフンに置き換えて -editor-script」というルールを思い出してください。editorScript なのに -script だけ付けてしまうのもよくある間違いです。

サイトの言語設定とロケールが合っていない

翻訳ファイル名のロケール部分(ja など)は、サイトのロケールと一致している必要があります。管理画面の「設定」→「一般」でサイトの言語を確認してください。加えて WordPress にはユーザーごとの言語設定もあるため、サイト全体は日本語でも自分のプロフィールが英語になっていると、管理画面は英語で表示されます。プロフィール画面の言語設定もあわせて確認しましょう。

まとめ

カスタムブロックの翻訳対応は、JS の文字列と PHP の文字列で読み込む経路が違うことを理解できれば見通しが良くなります。JS 側は @wordpress/i18n の __()・_x()・_n() で文字列を囲み(テキストドメインは常に最後の引数)、値を埋め込むときは sprintf() と組み合わせます。block.json の title や description は textdomain を書いておけば PHP 側で訳されます。

そのうえで、PHP 側から wp_set_script_translations( $handle, $domain, $path ) を register_block_type() のあとに呼び、ハンドル名は generate_block_asset_handle() で取得するのが安全です。翻訳ファイルは wp i18n make-pot で POT を作り、訳した PO から wp i18n make-json で JSON まで変換します。JS の翻訳は .mo ではなく JSON——ここを押さえておけば、「英語のまま直らない」で悩む時間はぐっと減るはずです。

参考ページ