WordPressのブロックテーマ(フルサイト編集対応テーマ)では、一般的なアプローチとは異なる方法で翻訳が処理されます。従来の翻訳機能付きPHPテンプレートファイルは、HTMLテンプレート、JavaScript搭載ブロック、サイトエディターでは機能しないため、これまでとは違う視点でWordPressブロックの多言語化システムを理解しなければなりません。

今回この記事では、WordPressのブロックテーマを国際化(多言語化対応)する方法、具体的にはブロックテーマにおける翻訳の問題とその解決方法、そして翻訳プラグインとの統合方法をご紹介します。

ブロックテーマで従来の翻訳方法が使えない理由とその解決策

ブロックテーマは、WordPressのPHPファイルの多くをブロックのマークアップを含むHTMLテンプレートに置き換えます。しかし、HTMLテンプレートが_()_e()のようなPHP翻訳関数を実行できません。結果として、既存の翻訳済み文字列が静的ファイルに置かれたまま使用できないことになります。

WordPress 6.8では、ブロックテーマの国際化を簡素化する改良がいくつか加えられており、例えば適切なText DomainDomain Pathヘッダー持つテーマでmanual load_theme_textdomain()の呼び出しが不要になりました。

代わりに翻訳ファイルを自動的に読み込み、パフォーマンス改善のためにテーマディレクトリよりもwp-content/languages/themes/ディレクトリを優先します。

まずは、style.cssファイルにメタデータを追加して、従来の方法でテーマを設定します。

/*
Theme Name: My Block Theme
Text Domain: my-block-theme
Domain Path: /languages
*/

注意点として、最近のWordPressのバージョンで翻訳ファイルが正しく読み込まれるには、Text Domainヘッダーがテーマのフォルダ名(通常はケバブケースで書かれる)と一致している必要があります。

style.cssと同様、functions.phpファイルのセットアップも最小限です。

<?php
// WordPress 6.8以降では任意だが、下位互換性を考慮して含める
function my_block_theme_setup() {
    load_theme_textdomain( 'my-block-theme', get_template_directory() . '/languages' );
}

add_action( 'after_setup_theme', 'my_block_theme_setup' );

// 翻訳対応のブロックスクリプトを登録
function my_block_theme_scripts() {
    wp_enqueue_script(
        'my-block-theme-scripts',
        get_template_directory_uri() . '/assets/js/theme.js',
        array( 'wp-i18n' ),
        '1.0.0',
        true
    );

    wp_set_script_translations( 
        'my-block-theme-scripts', 
        'my-block-theme', 
        get_template_directory() . '/languages' 
    );
}

add_action( 'wp_enqueue_scripts', 'my_block_theme_scripts' );

ここでのクラシックテーマとブロックテーマの重要な違いは、後者がサーバーサイドのPHPとクライアントサイドのJavaScriptに翻訳作業が分担される点です。クラシックテーマでは、ほとんどの翻訳をPHPに依存します。

block.jsonに翻訳を設定する方法

block.jsonファイルは、翻訳するブロックの「設定ハブ」になり、多言語化対応を適切に設定することで、エディター上でもフロントエンドでもブロックが正しく翻訳されます。

正規のブロック登録方法では、block.jsonを使用します。textdomainを設定しておくと、「title」「description」「keywords」フィールドが翻訳されます。

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "my-theme/testimonial",
	"title": "お客様の声",
	"category": "text",
	"description": "お客様の声を掲載",
	"keywords": ["引用", "口コミ", "お客様の声"],
	"textdomain": "my-block-theme",
	"attributes": {
		"content": {
			"type": "string",
			"source": "html",
			"selector": "blockquote"
		}
	}
}

ただし、コンテキストを必要とする場合にはサーバー側での登録が必要です。同じ単語でもコンテキストによって翻訳異なることがあり、例えば、ECサイトにおける「order」の日本語訳は、「注文」になる可能性もあれば、「順序」になる可能性もあります。

function my_theme_register_testimonial_block() {
	register_block_type_from_metadata(
		get_template_directory() . '/blocks/testimonial',
		array(
			'title' => _x( 'Testimonial', 'block title', 'my-block-theme' ),
			'description' => _x(
				'Display customer testimonials', 
				'block description', 
				'my-block-theme'
			),

			'keywords' => array(
				_x( 'quote', 'block keyword', 'my-block-theme' ),
				_x( 'review', 'block keyword', 'my-block-theme' )
			)
		)
	);
}

add_action( 'init', 'my_theme_register_testimonial_block' );

追加するブロックのバリエーションにも規則的な名前付けが必要です。WordPressは翻訳を読み込む際に特定のパターンを探すため、各バリエーション名は翻訳キーの一部として扱われます。

{
	"name": "my-theme/button",
	"title": "Button",
	"textdomain": "my-block-theme",
	"variations": [{
			"name": "primary",
			"title": "プライマリボタン",
			"attributes": {
				"className": "is-style-primary"
			}
		},
		{
			"name": "secondary",
			"title": "セカンダリボタン",
			"attributes": {
				"className": "is-style-secondary"
			}
		}
	]
}

JavaScriptの国際化では、サイトエディターはサーバー上ではなくブラウザ上で動作します。したがって、WordPressの国際化関数をインポートし、スクリプトの翻訳を設定する必要があります。PHPの翻訳関数はJavaScriptには存在しないため、WordPressは@wordpress/i18nパッケージを通して同等の関数を提供します。

import {
	registerBlockType
} from '@wordpress/blocks';
import {
	__
} from '@wordpress/i18n';
import {
	useBlockProps,
	RichText
} from '@wordpress/block-editor';

registerBlockType('my-theme/testimonial', {
	edit: ({
		attributes,
		setAttributes
	}) => {
		const blockProps = useBlockProps();

		return ( < div { ...blockProps } >
			< RichText tagName = "blockquote" value = { attributes.content } onChange = { (content) => setAttributes({
					content
				})
			}
			placeholder = {
				__('Add testimonial text...', 'my-block-theme')
			}
			/> < cite >
			< RichText tagName = "span" value = { attributes.author } onChange = { (author) => setAttributes({
					author
				})
			}
			placeholder = {
				__('Author name', 'my-block-theme')
			}
			/> < /cite> < /div>
		);
	}
});

加えて、WordPressはクライアントサイドの翻訳に異なるフォーマットを使用するため、JavaScript用のJSON翻訳ファイルを生成することをおすすめします。PHPは.moファイルを使用しますが、JavaScriptでは特定の命名規則で.jsonファイルが必要になります。これはWP-CLIコマンドを使って自動化可能です。

# JavaScriptファイルから文字列を抽出してPOTに保存
wp i18n make-pot . languages/my-block-theme.pot

# POファイルをJavaScript用のJSONに変換
wp i18n make-json languages/ --no-purge --pretty-print

この結果、JSONファイルは{textdomain}-{locale}-{handle}.jsonという一貫したパターンに従うようになります。WordPressでは、wp_set_script_translations()を呼び出す際にこれが読み込まれます。

静的なHTMLテンプレートを翻訳可能なPHPコンポーネントに変換する方法

HTMLテンプレートが静的であることを考慮すると、ブロックテーマの多言語化対応に使用することは困難です。従来の翻訳関数や手法はそのままでは使うことができません。

WordPressでは、HTMLテンプレートで参照されてもPHPのテンプレートパーツがPHPファイルとして処理されます。このハイブリッド型のアプローチで、ブロックテーマの構造を維持しつつ、動的コンテンツを作成することが可能です。できます。

<!-- templates/page.html -->
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->

<main class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
     <!-- wp:post-content /-->
     <!-- wp:template-part {"slug":"post-meta"} /-->
</main>
<!-- /wp:group →
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->

なお、テンプレートパーツはPHPを含むことができます。

<!-- parts/post-meta.html -->
<!-- wp:group {"className":"post-meta"} -->
<div class="wp-block-group post-meta">
<?php
    echo sprintf(
      /* translators: 1: Post date, 2: Post author */
      __( 'Published on %1$s by %2$s', 'my-block-theme' ),
      get_the_date(),
     get_the_author()
     );
     ?>
</div>
<!-- /wp:group -->

ブロック属性だけでは処理できない複雑なブロックには、サーバー側の処理を行うためにrender.phpファイルが必要です。データベースクエリ条件ロジック、動的コンテンツ生成にはすべてPHPの実行が求められます。

// blocks/recent-posts/render.php
<?php
$recent_posts = get_posts( array( 
'numberposts' => $attributes['count'] ?? 5 
) );
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<h3><?php echo esc_html__( 'Recent Posts', 'my-block-theme' ); ?></h3>
     <?php if ( $recent_posts ) : ?>
      <ul>
      <?php foreach ( $recent_posts as $post ) : ?>
           <li>
           <a href="<?php echo get_permalink( $post ); ?>">
            <?php echo get_the_title( $post ); ?>
           </a>

           <span class="post-date">
            <?php echo get_the_date( '', $post ); ?>
           </span>
           </li>
         <?php endforeach; ?>
         </ul>
     <?php else : ?>
         <p><?php esc_html_e( 'No posts found.', 'my-block-theme' ); ?></p>
     <?php endif; ?>
</div>

つまり、block.jsonのレンダーファイルを使用するようにブロックを設定します。

{
	"name": "my-theme/recent-posts",
	"render": "file:./render.php",
	"attributes": {
		"count": {
			"type": "number",
			"default": 5
		}
	}
}

カスタムフィールドとユーザー入力に動的コンテンツ翻訳を実装する方法

動的コンテンツは、多くのWordPressサイトで広く使用されていますが、 テーマファイルではなくデータベース上に存在するため、翻訳の問題を引き起こす可能性があります。サードパーティの翻訳プラグインを使用する場合は、静的なテーマの文字列とは別に動的コンテンツを識別、管理しなければなりません。

翻訳プラグインは、WordPressのメタシステムをフックして翻訳可能なコンテンツを検出するため、適切なメタ設定でカスタムフィールドを登録することが重要です。show_in_restパラメータがサイトエディターとの互換性を有効にします。

function my_theme_register_meta_fields() {
    register_post_meta( 'page', 'custom_subtitle', array(
        'type' => 'string',
        'description' => __( 'Page subtitle', 'my-block-theme' ),
        'single' => true,
        'show_in_rest' => true,
        'auth_callback' => function() {
            return current_user_can( 'edit_posts' );
        }
    ));
}

add_action( 'init', 'my_theme_register_meta_fields' );

// プラグイン互換性を表示
function my_theme_display_subtitle( $post_id ) {
    $subtitle = get_post_meta( $post_id, 'custom_subtitle', true );

    if ( ! $subtitle ) {
        return;
    }

    // WPML互換性
    // (wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/に記載)
    if ( function_exists( 'icl_t' ) ) {
        $subtitle = icl_t( 
            'my-block-theme', 
            'subtitle_' . $post_id, 
            $subtitle 
        );
    }

    // Polylang互換性
    // (polylang.pro/doc/function-reference/に記載)
    if ( function_exists( 'pll_translate_string' ) ) {
        $subtitle = pll_translate_string( $subtitle, 'my-block-theme' );
    }

    echo '<h2 class="page-subtitle">' . esc_html( $subtitle ) . '</h2>';
}

WordPressではコンテンツが言語別に自動でフィルタリングされることはないため、データベースクエリにも言語フィルタリングが必要になります。クエリの修正を追加する翻訳プラグインに対応しなければなりません。

function my_theme_get_localized_posts( $args = array() ) {
    $defaults = array(
        'post_type' => 'post',
        'posts_per_page' => 10
    );

    $args = wp_parse_args( $args, $defaults );

    // Polylangは言語タクソノミーを追加
    // (polylang.pro/doc/developpers-how-to/に記載)
    if ( function_exists( 'pll_current_language' ) ) {
        $args['lang'] = pll_current_language();
    }

    // WPMLは、suppress_filtersがfalseの場合にクエリを自動的にフィルタリング
    // (wpml.org/documentation/getting-started-guide/translating-custom-posts/に記載)

    if ( defined( 'ICL_LANGUAGE_CODE' ) ) {
        $args['suppress_filters'] = false;
    }

    return get_posts( $args );
}

またフォームの処理には、動的コンテンツと静的コンテンツが混在しますが、フォームのラベル、エラーメッセージ、管理者への通知などには、ユーザーの言語に対応できる翻訳が必要です。ユーザーの言語によってメールの受信者が変わることも考えられます。

function my_theme_process_contact_form() {
	if ( ! isset( $_POST['contact_nonce'] ) || 
	! wp_verify_nonce( $_POST['contact_nonce'], 'contact_form' ) ) {
	return;
	}

	$name = sanitize_text_field( $_POST['name'] );
	$email = sanitize_email( $_POST['email'] );
	$message = sanitize_textarea_field( $_POST['message'] );

	// 現在の言語で管理者メールを取得
	$admin_email = get_option( 'admin_email' );

	// 言語固有の管理者メールについては、WPMLの文字列翻訳を使用
	// (wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/に記載)

	if ( function_exists( 'icl_t' ) ) {
		// First register the string if not already registered
		if ( function_exists( 'icl_register_string' ) ) {
			icl_register_string( 'my-block-theme', 'contact_email', $admin_email );
		}

		$admin_email = icl_t(
			'my-block-theme',
			'contact_email',
			$admin_email
		);
	}

	$subject = sprintf(
	/* translators: %s: Sender name */
	__( 'Contact form submission from %s', 'my-block-theme' ),
	$name
	);

	wp_mail( $admin_email, $subject, $message );
}

add_action( 'init', 'my_theme_process_contact_form' );

メニュー項目、URL、構造も言語によって異なる可能性があるため、ナビゲーションの言語認識を評価することもお忘れなく。翻訳プラグインによっては、言語スイッチャーを作成するためのAPIが用意されています。

function my_theme_language_switcher_block() {
    if ( ! function_exists( 'pll_the_languages' ) && 
         ! function_exists( 'icl_get_languages' ) ) {
        return;
    }

    $output = '<div class="language-switcher">';

    // Polylangの言語スイッチャー
    // (polylang.pro/doc/function-reference/に記載)

    if ( function_exists( 'pll_the_languages' ) ) {
        $languages = pll_the_languages( array( 'raw' => 1 ) );
        foreach ( $languages as $lang ) {
            $output .= sprintf(
                '<a href="%s" class="%s">%s</a>',
                esc_url( $lang['url'] ),
                $lang['current_lang'] ? 'current-lang' : '',
                esc_html( $lang['name'] )
            );
        }
    }

    // WPMLの言語スイッチャー
    // (wpml.org/documentation/support/wpml-coding-api/multi-language-api/に記載)
    elseif ( function_exists( 'icl_get_languages' ) ) {
        $languages = icl_get_languages();
        foreach ( $languages as $lang ) {
            $output .= sprintf(
                '<a href="%s" class="%s">%s</a>',
                esc_url( $lang['url'] ),
                $lang['active'] ? 'current-lang' : '',
                esc_html( $lang['native_name'] )
            );
        }
    }

    $output .= '</div>';
    return $output;
}

通常、翻訳プラグインを使った多言語化対応には、多くの作業が求められます。最後に、翻訳プラグインとの連携方法をご紹介します。

翻訳プラグインとの連携方法(+互換性と最適化)

ブロックテーマの扱い方は、翻訳プラグインによって異なるため、使用するプラグインのアプローチをあらかじめ理解して、互換性と柔軟性を確保しましょう。

WPMLについては、フルサイト編集に関するドキュメントにブロックテーマに必要な設定が記載されています。

// WPML FSE compatibility based on official documentation
add_action( 'init', function() {
    if ( ! defined( 'WPML_VERSION' ) ) {
    return;
    }

    // FSEテーマはWPML 4.5.3以降で自動的に検出 // FSEサポートを有効化
    add_filter( 'wpml_is_fse_theme', '__return_true' );

    // WPML文字列翻訳のドキュメントに従ってカスタム文字列を登録
    // (wpml.org/documentation/support/wpml-coding-api/wpml-hooks-reference/に記載)

    if ( function_exists( 'icl_register_string' ) ) {
        icl_register_string(
        'my-block-theme',
        'footer-copyright',
        '© My Company.'
        );
    }
});

Polylang Proは、バージョン3.2からサイトエディターに対応しており、標準の文字列翻訳インターフェースでブロックテーマを扱います。

// 公式ドキュメントに基づいたPolylangの文字列登録
if ( function_exists( 'pll_register_string' ) ) {
	pll_register_string( 
		'Footer Copyright',
		'© My Company.',
		'my-block-theme',
		true // Multiline support
	);
}

TranslatePressに関しては、ドキュメントによると、パフォーマンス最適化のため特定の動的要素を除外する必要があります。

// 公式推奨に基づくTranslatePressの最適化
// (translatepress.com/docs/developers/に記載)
add_filter( 'trp_stop_translating_page', function( $stop, $url ) {
	// 公式ドキュメントに従い、管理画面とAPIリクエストをスキップ
	if ( is_admin() || wp_is_json_request() ) {
	return true;
	}

	// レンダリングの問題を引き起こす可能性のあるパターンプレビューURLをスキップ
    if ( strpos( $url, 'pattern-preview' ) !== false ) {
    	return true;
	}

	return $stop;
}, 10, 2 );

サードパーティのコードベース(プラグインなど)で作業する際のヒントとして、まずは翻訳関連の問題のデバッグする際は体系的なアプローチをとることをおすすめします。

// 翻訳問題のデバッグ支援ツール
function my_theme_debug_translations() {
    if ( ! WP_DEBUG || ! current_user_can( 'manage_options' ) ) {
    return;
	}

	error_log( 'Text domain loaded: ' . is_textdomain_loaded(
    	'my-block-theme' ) );
         error_log( 'Current locale: ' . get_locale() );
         error_log( 'Translation test: ' . __(
			'Hello World',
            'my-block-theme'
         )
	);

	// ブロックのJSON翻訳を確認
    $json_file = WP_LANG_DIR . '/themes/my-block-theme-' . get_locale() . '-script-handle.json';
	error_log( 'JSON translation exists: ' . file_exists( $json_file ) );
}

add_action( 'init', 'my_theme_debug_translations' );

また、サイトのキャッシュは、翻訳の更新を妨げる可能性があるため、翻訳ファイルの変更時にクリアするのが得策です。

# WordPressのTransientsをクリア
wp transient delete --all

# 新しい翻訳ファイルを生成
wp i18n make-pot . languages/my-block-theme.pot
wp i18n make-json languages/ --no-purge

翻訳プラグインを使う場合、パフォーマンス最適化も非常に重要です。プラグインごとにデータベースクエリや処理負荷が増えるため、頻繁に使う翻訳はキャッシュしておくと効果的です。

function my_theme_cached_translation( $text, $domain = 'my-block-theme' ) {
    $cache_key = 'translation_' . md5( $text . get_locale() );
    $cached = wp_cache_get( $cache_key, 'my_theme_translations' );

    if ( false === $cached ) {
        $cached = __( $text, $domain );
        wp_cache_set( $cache_key, $cached, 'my_theme_translations', HOUR_IN_SECONDS );
    }

    return $cached;
}

あるいは、デプロイの準備が整うまでキャッシュをスキップするのが賢明かもしれません。この場合はステージング環境を使用するのが好ましく、キャッシュによるパフォーマンス改善は基本的に不要です。

まとめ

ブロックテーマの国際化(多言語化対応)には、WordPress従来の翻訳方法に加えて、サイトエディター内の新たな手法も利用する必要があります。

テーマのメタデータを設定し、テンプレート戦略を実装して、翻訳プラグインの要件を把握することで、パフォーマンスと質の高いユーザー体験を提供する多言語対応ブロックテーマを構築できます。

KinstaのWordPress専用マネージドクラウドサーバーは、組み込みのキャッシュ、37箇所の拠点を持つCDN、Git統合やステージング環境などの機能により、ウェブサイトが必要とするパフォーマンスとグローバルリーチを提供しています。その性能をぜひ一度お試しください。

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。