WordPress Logo WordPress プラグインの作り方と設定ページの作成

基本的な WordPress のプラグインの作り方とその設定ページの作成方法についての解説のような覚書です。シンプルなプラグインを作成し、設定ページを追加したり、カスタムフィールドを設定します。

フロントエンドと管理画面(設定ページ)での CSS や JavaScript の読み込み方法やカスタム投稿タイプとカスタムタクソノミーを登録するプラグインの作り方、国際化対応などについても掲載しています。

この時点での WordPress のバージョンは 6.7.2 です。

更新日:2025年03月11日

作成日:2025年03月07日

以下ではすでにローカル環境( XAMPP や MAMP、Local by Flywheel、wp-env などのいずれか)がインストールされていて、ローカル環境での作業を前提にしています。

WordPress プラグインとは

WordPress プラグインは、WordPress サイトにインストールして 新しい機能を追加 したり、既存の機能を拡張 できるコードのパッケージです。

テーマ はサイトの外観やデザインを制御するのに対し、プラグインはサイトの機能をカスタマイズするために使用されます。

プラグインを使用する理由

WordPress サイトにカスタム機能を追加する方法として、テーマの functions.php にコードを記述することも可能ですが、次の理由から、独立したプラグインとして作成する方が推奨されます。

  • テーマに依存しない:テーマを変更しても機能が維持される
  • 有効化・無効化が簡単:必要に応じてプラグイン単位で機能を管理できる
  • 再利用が可能:同じプラグインを複数のサイトで使用できる
  • 保守・更新がしやすい:テーマとは独立して管理できるため、バージョン管理やアップデートが容易

プラグインの構造

WordPress プラグインは、wp-content/plugins ディレクトリ内に保存されます。

ほとんどのプラグインは複数のファイルで構成されますが、最低限必要なのは、適切なプラグインヘッダーを記述したメイン PHP ファイルです。

wp-content/
  └─ plugins/
      └─ my-custom-plugin/  ← プラグインフォルダ
          └─ my-custom-plugin.php  ← メインファイル(必須)
wp-content/
  └─ plugins/
      └─ my-custom-plugin/  ← プラグインフォルダ
          ├─ my-custom-plugin.php  ← メインファイル(必須)
          ├─ includes/  ← 機能を分割するためのディレクトリ
          ├─ assets/  ← CSS や JS などのリソース
          ├─ templates/  ← カスタムテンプレートファイル
          └─ readme.txt  ← WordPress.org 用の説明ファイル

プラグインフォルダ名とメインファイル名

プラグインフォルダ名とメイン PHP ファイルの名前は必ずしも同じである必要はありませんが、一般的に同じ名前にします。それにより、管理しやすくなり、どのファイルがメインであるかが明確になります。

プラグインヘッダー

WordPress は、プラグインのメインファイルに「プラグインヘッダー」があるかどうかで、それをプラグインとして認識します。

プラグインヘッダーは、プラグインの名前やバージョン、作者情報などのメタ情報を含む特定の形式のコメントブロックです。

このヘッダーがないファイルは、WordPress によってプラグインとして認識されません。

そのため、プラグインのメインファイルには必ずプラグインヘッダーを記述する必要があります。

WordPress がプラグインを認識する仕組み

  1. WordPress は wp-content/plugins/ ディレクトリをスキャンする。
  2. 各プラグインフォルダ内の最上位レベルの PHP ファイルを確認する。
  3. ファイルの最初の数行にプラグインヘッダーがあるかどうか をチェックする。
  4. プラグインヘッダーが見つかった場合、そのファイルをプラグインのメインファイルとして登録する。

有効なプラグインヘッダーの例

プラグインヘッダーは、PHP の開始タグ(<?php)の直後に、/** で始まり */ で終わるマルチラインコメントの形式で記述します。

<?php

/**
 * Plugin Name: My Custom Plugin
 * Plugin URI: https://example.com
 * Description: このプラグインの説明
 * Version: 1.0.0
 * Author: 作者名
 * Author URI: https://example.com
 * License: GPL-2.0+
 * Text Domain: my-custom-plugin
 */

Plugin Name フィールドのみが必須です。Plugin Name は、プラグインの管理画面などに表示される名前であり、フォルダ名やメインファイル名と同じである必要はありません。

一般的には、必須の Plugin Name 以外に Description と Version も追加することが推奨されます。Plugin Name、Description、Version、Author の値はプラグインリストに表示されます。

また、Text Domain は、WordPress の国際化と翻訳に使用される識別子で、通常、プラグインのフォルダ名と一致させます。

上記のプラグインヘッダーにより、プラグインリストには以下のように表示されます。

Plugin Name、Description、Version、Author に指定した値が表示され、Plugin URI を指定した場合は「プラグインのサイトを表示」というリンクが表示され、Author URI を指定した場合は、作者名にその URL のリンクが設定されます。

プラグインヘッダーのフィールの詳細は以下で確認できます。

Plugin Handbook: Header Requirements

ベストプラクティス

プラグインハンドブック(Plugin Handbook)には、プラグインを開発する際の一般的なベストプラクティスに関するセクションがあります。

その1つは、プラグインコードが WordPress リクエストの一部である場合にのみ実行されるようにするための以下のチェックをプラグインヘッダーの直後に含めることです。

// セキュリティ対策(必須)
if (! defined('ABSPATH')) {
  exit; // 直接アクセスされた場合は終了
}

上記のコードは、WordPress 固有の定数である ABSPATH 定数が定義されているかどうかを確認し、定義されていない場合は、プラグインコードの実行を終了します。

これにより、誰かがブラウザでメインプラグインファイルを直接参照しようとしても、プラグイン内の PHP コードは実行されず、セキュリティリスクを回避できます。

ベストプラクティスの詳細は以下で確認できます。

Plugin Handbook: Best Practices

プラグインを作ってみる

プラグインディレクトリ(wp-content/plugins/)に my-first-plugin というディレクトリを作成し、その中に my-first-plugin.php というメインのプラグインファイルを作成します。

plugins/
  └── my-first-plugin/
      └── my-first-plugin.php

my-first-plugin.php に以下のプラグインヘッダーとセキュリティ対策のコードを記述します。

<?php

/**
 * Plugin Name: My First Plugin
 * Description: A plugin to show message.
 * Version: 0.1.0
 * Text Domain: my-first-plugin
 */

 if ( ! defined( 'ABSPATH' ) ) {
  exit; // 直接アクセスされた場合は終了
}

// プラグインの処理を記述

管理画面のプラグインページを開くと、上記プラグインが表示されるので「有効化」をクリックします。

まだ、処理を何も記述していないので、何も起きません。

プラグインファイルを書き換えて、投稿の最後にメッセージを表示するようにます。

定義した関数を the_content フィルターにフックして投稿の最後にメッセージを追加しています。

関数名にはプレフィックス(my_first_plugin_)を追加して、他のプラグインと衝突しないようにします。

関数の処理では、is_single() と is_main_query() でメインクエリの投稿ページのみ対象にしています。

追加する文字列は esc_html__() を使ってエスケープ処理し、翻訳可能にしています。第2引数には Text Domain の値を指定します。

CSS クラスにもプレフィックス(my-first-plugin-)を付けて他のテーマやプラグインとの競合を防ぎます。

<?php

/**
* Plugin Name: My First Plugin
* Description: A plugin to show message.
* Version: 0.1.0
* Text Domain: my-first-plugin
*/

if (! defined('ABSPATH')) {
  exit; // 直接アクセスされた場合は終了
}

/**
* 投稿の最後にメッセージを追加するフィルター(フロントエンドへの出力)
*
* @param string $content 投稿コンテンツ
* @return string 変更後のコンテンツ
*/
function my_first_plugin_add_thanks_message($content) {

  // メインクエリの投稿ページのみ対象
  if (is_single() && is_main_query()) {
    $message = '<p class="my-first-plugin-message">' . esc_html__('Thank you for reading!', 'my-first-plugin') . '</p>';
    $content .= $message;
  }
  return $content;
}
add_filter('the_content', 'my_first_plugin_add_thanks_message');

以下は上記で使用している関数とフィルターの説明です。

関数・フィルター 説明
the_content 投稿の本文 (post_content) を変更できるフィルター。ショートコードの処理やカスタムメッセージの追加などに使用。
is_single() 投稿の個別ページ(シングル投稿ページ)で true を返す条件分岐タグ。アーカイブページや固定ページでは false になる。
is_main_query() メインクエリ(メインループ)で true を返す。ウィジェットやカスタムクエリには適用されないように制御するのに役立つ。
esc_html__() 翻訳可能な文字列を出力する関数。HTMLエスケープを行い、安全な表示を保証。

このプラグインにより、例えば、以下のように全ての投稿ページのコンテンツの最後にメッセージ「Thank you for reading!」が表示されます。

カスタム設定ページ

前述の例ではプラグインファイルにハードコードしたメッセージを表示するものでしたが、管理画面に独自の設定ページを作成してメッセージを編集できるようにします。

プラグインファイル my-first-plugin.php を以下のように書き換えます。

my_first_plugin_get_message() は設定ページでメッセージが入力されていれば、その値を返し、空の場合はデフォルトメッセージ「Thank you for reading!」を返す関数です。

表示するメッセージは register_setting() を使って my_first_plugin_message というオプション名で保存するので(74行目)、get_option() に 'my_first_plugin_message' を指定して取得します。

投稿の最後にメッセージを追加するフィルターの処理では、my_first_plugin_get_message() で取得したメッセージを wp_kses_post() でサニタイズし、div 要素でラップして投稿に追加しています。

続く処理はカスタム設定ページを作成するものです(後述)。

<?php

/**
* Plugin Name: My First Plugin
* Description: A plugin to show a customizable message at the end of posts.
* Version: 0.1.1
* Text Domain: my-first-plugin
*/

if (!defined('ABSPATH')) {
  exit; // 直接アクセスされた場合は終了
}

// メッセージの値を取得して返す関数
function my_first_plugin_get_message() {
  return get_option('my_first_plugin_message') ? get_option('my_first_plugin_message') : 'Thank you for reading!';
}

/**
* 投稿の最後にメッセージを追加するフィルター(フロントエンドへの出力)
*
* @param string $content 投稿コンテンツ
* @return string 変更後のコンテンツ
*/
function my_first_plugin_add_message($content) {
  // メインクエリの投稿ページのみ対象
  if (is_single() && is_main_query()) {
    // 上記で定義した関数でメッセージを取得
    $message = my_first_plugin_get_message();
    // サニタイズしたメッセージを div 要素でラップしてコンテンツに追加
    $content .= '<div class="my-first-plugin-message">'. wp_kses_post($message) .'</div>';
  }
  return $content;
}
add_filter('the_content', 'my_first_plugin_add_message');

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_first_plugin_add_admin_menu() {
  add_options_page(
    __('My First Plugin Settings', 'my-first-plugin'),
    __('My First Plugin', 'my-first-plugin'),
    'manage_options',
    'my-first-plugin',
    'my_first_plugin_settings_page'
  );
}
add_action('admin_menu', 'my_first_plugin_add_admin_menu');

/**
* 設定ページの出力(設定ページを描画するコールバック関数を定義)
*/
function my_first_plugin_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My First Plugin Settings', 'my-first-plugin'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_first_plugin_options');
      do_settings_sections('my-first-plugin');
      submit_button();
      ?>
    </form>
  </div>
<?php
}

/**
* 設定項目の登録
*/
function my_first_plugin_settings_init() {

  register_setting('my_first_plugin_options', 'my_first_plugin_message', ['sanitize_callback' => 'wp_kses_post']);

  add_settings_section(
    'my_first_plugin_section',
    __('Message Settings', 'my-first-plugin'),
    function () {
      echo '<p>'.__('Customize the message', 'my-first-plugin').'</p>';
    },
    'my-first-plugin'
  );

  add_settings_field(
    'my_first_plugin_message_textarea',
    __('Message to display', 'my-first-plugin'),
    'my_first_plugin_message_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );
}
add_action('admin_init', 'my_first_plugin_settings_init');

/**
* 設定フィールドの出力(入力用テキストエリアと説明を表示)
*/
function my_first_plugin_message_field_callback() {
  $message = my_first_plugin_get_message();
?>
  <textarea name="my_first_plugin_message" rows="5" cols="50" class="large-text code"><?php echo esc_textarea($message); ?></textarea>
  <p class="description"><?php esc_html_e('You can use HTML in this message.', 'my-first-plugin'); ?></p>
<?php
}

「設定」メニューに「My First Plugin」が追加され、クリックすると以下のようなカスタム設定ページが表示されます。

get_option()

get_option() は、WordPress のデータベースからオプション値を取得するための関数です。

get_option( string $option, mixed $default_value = false ): mixed
引数 説明
$option 取得したいオプションのキー(オプション名)
$default_value データベースにオプションが存在しない場合のデフォルト値。初期値: false

指定したオプションが存在せず、デフォルト値も提供されていない場合は、ブール値 false が返されます。

基本的な使い方

取得したいオプションのキー(オプション名)を引数に指定します。$value には、データベースから取得したオプションの値が入ります。オプションがデータベースに存在しない場合、false が返されます。

$value = get_option( 'option_name' );

デフォルト値を設定するには、get_option() の第二引数を使用します。以下では custom_option が存在しない場合、'default_value' が返ります。

$value = get_option( 'custom_option', 'default_value' );

get_option() の動作

  • wp_options テーブルから option_name に対応する option_value を取得する。
  • wp_cache(オブジェクトキャッシュ)に保存されていれば、データベースクエリをスキップしてキャッシュから取得する。
  • データが見つからなければ false を返す。

オプション値の設定・更新 update_option()

get_option() で取得するオプション値は update_option() で設定・更新できます。

update_option() はオプションがすでに存在すれば値を更新し、存在しない場合は自動的に追加します。

update_option( 'my_plugin_setting', 'enabled' );
$value = get_option( 'my_plugin_setting' ); // 'enabled' を取得

オプション値の新規追加 add_option()

add_option() は、指定したオプションが まだデータベースに存在しない場合にのみ追加します。

以下の場合、my_plugin_setting というオプションがすでに存在する場合は追加されず、処理はスキップされます。

add_option( 'my_plugin_setting', 'enabled' );

配列・オブジェクトの取得

オプション値として配列やオブジェクトも保存・取得できます。

$settings = array(
  'color' => 'blue',
  'font_size' => '14px'
);
update_option( 'theme_settings', $settings );

$saved_settings = get_option( 'theme_settings', array() );
echo $saved_settings['color']; // blue

上記の、get_option( 'theme_settings', array() ) で第2引数に array() を指定している理由は、theme_settings がまだ保存されていない場合に false ではなく空の配列を返させるためです。

第2引数のデフォルト値を指定しない場合、theme_settings がデータベースに存在しないと false が返されます。そのため、配列を期待する後続の処理でエラーになってしまいます。

配列を期待する場合は get_option('theme_settings', array()) のように明示的に空の配列を指定します。

カスタム設定ページでの設定(オプション)の保存

カスタム設定ページでのオプションの保存では、register_setting() でオプションを登録し、設定ページの「変更を保存」ボタンがクリックされると、フォームが options.php に対して POST 送信されます。このとき、update_option() が自動的に呼ばれ、データが wp_options テーブルに保存されます。

そのため、開発者が手動で update_option() を実行する必要はありません。

add_options_page()

add_options_page() は「設定」メニューにカスタム設定ページを追加する関数で、以下が構文です。

add_options_page(
  string $page_title,
  string $menu_title,
  string $capability,
  string $menu_slug,
  callable $callback
  int $position
);
引数 説明
$page_title 設定ページのタイトル(head 内の title タグに出力される文字列)
$menu_title 「設定」メニュー内に表示されるタイトル
$capability 設定ページにアクセスできるユーザーの権限
$menu_slug URL 識別用のスラッグ(ユニークな文字列)
$callback 設定ページのHTMLを描画する関数
$position メニュー内での表示位置を表す整数値。値が小さいほど上に表示。
  • $capability は通常 manage_options にします。これは管理者 (administrator) のみがアクセスできる設定ページを作るための権限です。
  • $menu_slug は他のプラグインと競合しないよう、ユニークな名前にすることが推奨されます。

※ add_options_page() は WordPress が管理画面のメニューを生成するタイミングで実行される必要があるため、admin_menu にフックします。

この例では以下のように設定しています。

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_first_plugin_add_admin_menu() {
  add_options_page(
    __('My First Plugin Settings', 'my-first-plugin'), // ページタイトル
    __('My First Plugin', 'my-first-plugin'), // メニュータイトル
    'manage_options',  // アクセス権限
    'my-first-plugin', // スラッグ
    'my_first_plugin_settings_page' // コールバック関数
  );
}
add_action('admin_menu', 'my_first_plugin_add_admin_menu');
設定ページの出力

設定ページの出力は、add_options_page() の第5引数に指定したコールバック関数を定義して設定ページのHTMLを描画します。コールバック関数の役割は以下になります。

  1. ページタイトルを表示
  2. 設定を保存するためのフォームを作成
  3. 隠しセキュリティフィールド(nonce)を出力
  4. 設定項目を表示
  5. 「変更を保存」ボタンを追加

以下はこの例の場合のコールバック関数 my_first_plugin_settings_page() です。

全体を管理画面のデフォルトのスタイルを適用するためのコンテナ(div.wrap)で囲みます。

フォームでは POST メソッドを使用し、action="options.php" を指定して設定項目のデータを WordPress の options.php に送信して、入力された設定を保存します。

フォームのコンテンツには、settings_fields() で隠しセキュリティフィールドを、do_settings_sections() で入力フィールドを含むセクションを、submit_button() で「変更を保存」ボタンを出力します。

/**
* 設定ページの出力(設定ページを描画するコールバック関数を定義)
*/
function my_first_plugin_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My First Plugin Settings', 'my-first-plugin'); ?></h1> <!-- ページタイトル -->
    <form method="post" action="options.php">
      <?php
        settings_fields('my_first_plugin_options'); // セキュリティ用の隠しフィールド
        do_settings_sections('my-first-plugin'); // 設定項目の表示
        submit_button(); // 「変更を保存」ボタン
      ?>
    </form>
  </div>
<?php
}

settings_fields()

settings_fields() は フォームの隠しセキュリティフィールド(nonce)を出力します。引数には、register_setting() の第1引数に指定する値(設定グループの識別名)を指定します。

do_settings_sections()

do_settings_sections() は、次項の「設定項目の登録」で add_settings_section() と add_settings_field() を使って追加するすべてのセクションとフィールドを出力します。引数にはこれらを出力するページのスラッグを指定します。

submit_button()

submit_button() は「変更を保存」ボタンを追加します。

設定項目の登録

まず最初に register_setting() を使って、フォームが送信された際に設定をデータベースに保存するために設定項目を登録します。

そして add_settings_section() で管理画面の設定ページ内のセクションを追加し、add_settings_field() でセクション内の入力フィールドを追加します。

register_setting(), add_settings_section(), add_settings_field() は管理画面の初期化を行うタイミングの admin_init にフックします。

/**
* 設定項目の登録
*/
function my_first_plugin_settings_init() {

  // 設定項目を登録
  register_setting('my_first_plugin_options', 'my_first_plugin_message', ['sanitize_callback' => 'wp_kses_post']);

  // 設定ページにセクション(大枠)を追加
  add_settings_section(
    'my_first_plugin_section',
    __('Message Settings', 'my-first-plugin'),
    function () {
      echo '<p>'.__('Customize the message', 'my-first-plugin').'</p>';
    },
    'my-first-plugin'
  );

  // セクション内に個々の入力フィールドを追加
  add_settings_field(
    'my_first_plugin_message',
    __('Message to display', 'my-first-plugin'),
    'my_first_plugin_message_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );
}
add_action('admin_init', 'my_first_plugin_settings_init');
register_setting()

register_setting() は WordPress の設定(オプション)をデータベースに保存・管理するために設定を登録する関数です。この関数を使わないと、options.php へフォームを送信しても設定が保存されません。

以下が基本構文です。

register_setting( $option_group, $option_name, $args );
引数 説明
$option_group 設定グループの識別名(必須)
$option_name データベースに保存する設定名(必須)
$args オプションの引数。WordPress 4.7 から第3引数が拡張され、設定オプションを連想配列で渡せるようになっていて、以下のオプションが指定できます。
  • sanitize_callback : データを保存する前にサニタイズする関数
  • show_in_rest : true にすると REST API に公開される
  • default : 値が未設定の場合のデフォルト値

この例では以下のように設定しています。

これにより、フォームに入力された値がサニタイズされ、my_first_plugin_message というオプション名でデータベースの wp_options テーブルに保存され、保存された値は get_option('my_first_plugin_message') で取得できます。

以下では sanitize_callback に wp_kses_post を指定していますが、はサニタイズする内容により、esc_attresc_htmlsanitize_text_field などのエスケープ処理の関数や独自の関数を定義して指定することができます。

// 設定項目を登録
register_setting(
  'my_first_plugin_options', // 設定グループの識別名
  'my_first_plugin_message', // データベースに保存する設定名(オプション名)
  ['sanitize_callback' => 'wp_kses_post']  // 値をサニタイズするコールバック
);

フォーム送信時の処理の流れ

以下は設定ページで「変更を保存」ボタンがクリックされた際に内部的に行われる処理の流れです。

  1. options.php にリクエストが送信される
  2. register_setting() で登録されたオプションがチェックされる
  3. バリデーション・サニタイズ(sanitize_callback があれば実行)
  4. update_option( 'オプション名', $_POST['オプション名'] ) が実行される
  5. データベースの wp_options テーブルに保存される
  6. 設定ページにリダイレクトされ、「設定を保存しました。」というメッセージが表示される

add_settings_section()

add_settings_section() は、管理画面の「設定」ページにセクションを追加するための関数です。この関数を使用することで、関連する設定フィールドをグループ化し、整理された UI を提供できます。

以下が構文です。

add_settings_section(
  string $id,
  string $title,
  callable $callback,
  string $page
);
パラメータ 説明
$id セクションの一意の識別子(ID)
$title セクションの見出し(設定ページに表示)
$callback セクションのコンテンツを出力するコールバック関数。出力しない場合は '__return_false'
$page セクションを追加する設定ページのスラッグ。add_options_page() の $menu_slug

この例では以下のように設定しています。

セクションの説明などのコンテンツを出力するコールバック関数には直接コールバック関数を記述していますが、別途定義することもできます。また、説明を出力しない場合は、'__return_false' と指定することができます。

第4パラメータの $page は、どの設定ページにセクションを追加するかを指定するもので、add_options_page() のスラッグ($menu_slug)と一致させます。

add_settings_section(
  'my_first_plugin_section', // セクションの一意の識別子
  __('Message Settings', 'my-first-plugin'), // セクションのタイトル
  function () {
    echo '<p>'.__('Customize the message', 'my-first-plugin').'</p>';
  },
  'my-first-plugin' // ページスラッグ( add_options_page() のスラッグに指定した値)
);
add_settings_field()

add_settings_field() は設定ページのセクション内に入力フィールドを追加する関数です。

以下が構文です。

add_settings_field(
  string $id,
  string $title,
  callable $callback,
  string $page,
  string $section
  array $args = []
);
パラメータ 説明
$id 設定フィールドの一意の識別子(ID)。
$title ラベル(タイトル)。設定ページにフィールドの見出しとして表示される。
$callback 入力フィールドを出力する関数。この関数の中で input や select などを echo する。
$page 設定ページのスラッグ。add_options_page() で定義したスラッグと一致させる。
$section フィールドを配置するセクションの ID。add_settings_section() で登録したもの。
$args 追加の引数(オプション)。$callback に渡されるデータ。

この例では以下のように設定しています。

add_settings_field(
  'my_first_plugin_message_textarea',  // 設定フィールドの識別子
  __('Message to display', 'my-first-plugin'),  // ラベル(タイトル)
  'my_first_plugin_message_field_callback',  // 入力フィールドを出力する関数
  'my-first-plugin', // 設定ページのスラッグ
  'my_first_plugin_section' // フィールドを配置するセクションの ID
);
入力フィールドの出力

以下は add_settings_field() の第3パラメータに指定した入力フィールドを出力する関数です。

この例ではメッセージを入力するためのテキストエリアとその説明文を出力しています。

入力フィールドの要素の name 属性には、register_setting() の $option_name(データベースに保存するオプション名)を指定します。※ フォームで POST 送信するので、入力フィールドの要素には name="オプション名" を設定する必要があります。

入力フィールドのテキストエリアには my_first_plugin_get_message() で取得したメッセージを esc_textarea() でエスケープして出力します(フォームが送信されるとクリアされるため)。

/**
* 設定フィールドの出力(入力用テキストエリアと説明を表示)
*/
function my_first_plugin_message_field_callback() {
  // my_first_plugin_get_message() でメッセージを取得
  $message = my_first_plugin_get_message();
?>
  <textarea name="my_first_plugin_message" rows="5" cols="50" class="large-text code"><?php echo esc_textarea($message); ?></textarea>
  <p class="description"><?php esc_html_e('You can use HTML in this message.', 'my-first-plugin'); ?></p>
<?php
}

上記では、textarea 要素には large-text クラスを指定して、テキストエリアの幅を広げ(100% 幅)、code クラスにより、テキストが等幅フォント(monospace)で表示されるようにしています。

また、説明文には description クラスを指定して、テキストをグレーっぽく表示しています。

input 要素や textarea 要素に指定できるクラスには以下のようなものがあります(textarea には適用されないものも含まれます)。

クラス名 効果
regular-text 通常のテキストフィールド(デフォルトのサイズ、width: 25em)
small-text 小さい入力フィールド(width: 4em)
medium-text 中程度の入力フィールド(width: 10em)
large-text 幅が広い入力フィールド(width: 100%)
code 等幅フォント(monospace)で表示
hidden 非表示(CSS の display: none; が適用)

独自のクラスを指定して、別途 CSS を設定ページ用に読み込みスタイルを指定することもできます。

プレビューとカスタム CSS 設定の追加

管理画面設定ページで、メッセージを保存する前にリアルタイムで確認できるようにプレビューセクションを追加し、メッセージのデザインを変更できるように CSS のカスタマイズオプションを追加します。

my-first-plugin.php を以下のように書き換えます。

設定ページを出力するコールバック関数で、プレビューセクションの HTML を追加し、メッセージが入力される textarea 要素とカスタム CSS が入力される input 要素の input イベントを監視してプレビューに反映する JavaScript を追加します(71-90行目)。

設定項目の登録では、register_setting() でカスタム CSS を入力するためのオプション my_first_plugin_message_style を登録し(99行目)、add_settings_field() でスタイルの入力フィールドを追加します(119-124行目)。

そしてスタイルの入力フィールドを input 要素で出力します(141-147行目)。

また、get_option() を使ったスタイルの値の取得では、何も入力がない場合は、デフォルトのスタイルとして 'color: #666; font-size: 16px;' を設定しています(21行目)。

<?php

/**
* Plugin Name: My First Plugin
* Description: A plugin to show a customizable message at the end of posts.
* Version: 0.1.2
* Text Domain: my-first-plugin
*/

if (!defined('ABSPATH')) {
  exit; // 直接アクセスされた場合は終了
}

// メッセージの値を取得して返す関数
function my_first_plugin_get_message() {
  return get_option('my_first_plugin_message') ? get_option('my_first_plugin_message') : 'Thank you for reading!';
}

// スタイルの値を取得して返す関数
function my_first_plugin_get_message_style() {
  return get_option('my_first_plugin_message_style') ? get_option('my_first_plugin_message_style') : 'color: #666; font-size: 16px;';
}

/**
* 投稿の最後にメッセージを追加するフィルター(フロントエンドへの出力)
*
* @param string $content 投稿コンテンツ
* @return string 変更後のコンテンツ
*/
function my_first_plugin_add_message($content) {
  if (is_single() && is_main_query()) {
    $message = my_first_plugin_get_message();
    // スタイルの値を取得
    $style = my_first_plugin_get_message_style();
    // スタイルの値を style 属性に設定
    $content .= '<div class="my-first-plugin-message" style="' . esc_attr($style) . '">' . wp_kses_post($message) . '</div>';
  }
  return $content;
}
add_filter('the_content', 'my_first_plugin_add_message');

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_first_plugin_add_admin_menu() {
  add_options_page(
    __('My First Plugin Settings', 'my-first-plugin'),
    __('My First Plugin', 'my-first-plugin'),
    'manage_options',
    'my-first-plugin',
    'my_first_plugin_settings_page'
  );
}
add_action('admin_menu', 'my_first_plugin_add_admin_menu');

/**
* 設定ページの出力
* プレビューセクションとプレビューを更新する JavaScript を追加
*/
function my_first_plugin_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My First Plugin Settings', 'my-first-plugin'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_first_plugin_options');
      do_settings_sections('my-first-plugin');
      submit_button();
      ?>
    </form>
    <h2><?php esc_html_e('Live Preview', 'my-first-plugin'); ?></h2>
    <div id="my-first-plugin-preview-wrapper" style="padding: 10px; border: 1px solid #ccc; background: #fff;">
      <div id="my-first-plugin-preview" style="<?php echo esc_attr(my_first_plugin_get_message_style()); ?>">
        <?php echo wp_kses_post(my_first_plugin_get_message()); ?>
      </div>
    </div>
  </div>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      let textarea = document.querySelector('[name="my_first_plugin_message"]');
      let styleInput = document.querySelector('[name="my_first_plugin_message_style"]');
      let preview = document.getElementById('my-first-plugin-preview');
      textarea.addEventListener('input', function() {
        preview.innerHTML = textarea.value;
      });
      styleInput.addEventListener('input', function() {
        preview.style = styleInput.value;
      });
    });
  </script>
<?php
}

/**
* 設定項目の登録
*/
function my_first_plugin_settings_init() {
  register_setting('my_first_plugin_options', 'my_first_plugin_message', ['sanitize_callback' => 'wp_kses_post']);
  register_setting('my_first_plugin_options', 'my_first_plugin_message_style', ['sanitize_callback' => 'sanitize_text_field']);

  add_settings_section(
    'my_first_plugin_section',
    __('Message Settings', 'my-first-plugin'),
    function () {
      echo '<p>' . __('Customize the message', 'my-first-plugin') . '</p>';
    },
    'my-first-plugin'
  );

  add_settings_field(
    'my_first_plugin_message_textarea',
    __('Message to display', 'my-first-plugin'),
    'my_first_plugin_message_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );

  // スタイルの入力フィールド
  add_settings_field(
    'my_first_plugin_message_style',
    __('Message CSS Style', 'my-first-plugin'),
    'my_first_plugin_message_style_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );
}
add_action('admin_init', 'my_first_plugin_settings_init');

/**
* 設定フィールドの出力
*/
function my_first_plugin_message_field_callback() {
  $message = my_first_plugin_get_message();
?>
  <textarea name="my_first_plugin_message" rows="5" cols="50" class="large-text code"><?php echo esc_textarea($message); ?></textarea>
  <p class="description"><?php esc_html_e('You can use HTML in this message.', 'my-first-plugin'); ?></p>
<?php
}

// スタイルの入力フィールドの出力
function my_first_plugin_message_style_field_callback() {
  $style = my_first_plugin_get_message_style();
?>
  <input type="text" name="my_first_plugin_message_style" value="<?php echo esc_attr($style); ?>" class="large-text code">
  <p class="description"><?php esc_html_e('Enter custom CSS styles for the message (e.g., "color: red; font-size: 20px;").', 'my-first-plugin'); ?></p>
<?php
}

カスタムフィールドを使って制御

これまでの例の場合、全ての投稿ページでメッセージが表示されます。

以下は、投稿編集画面でカスタムフィールド show-message に 1 が設定されいる場合にのみ、メッセージを表示するように変更したものです。

get_post_meta(get_the_ID(), 'show-message', true) を使ってカスタムフィールド show-message の値を取得し(32行目)、値が '1' として保存されている場合だけメッセージ表示します(35行目)。

<?php

/**
* Plugin Name: My First Plugin
* Description: A plugin to show a customizable message at the end of posts.
* Version: 0.2.0
* Text Domain: my-first-plugin
*/

if (!defined('ABSPATH')) {
  exit;
}

function my_first_plugin_get_message() {
  return get_option('my_first_plugin_message') ? get_option('my_first_plugin_message') : 'Thank you for reading!';
}

function my_first_plugin_get_message_style() {
  return get_option('my_first_plugin_message_style') ? get_option('my_first_plugin_message_style') : 'color: #666; font-size: 16px;';
}

/**
* 投稿の最後にメッセージを追加するフィルター(フロントエンドへの出力)
*
* @param string $content 投稿コンテンツ
* @return string 変更後のコンテンツ
*/
function my_first_plugin_add_message($content) {
  if (is_single() && is_main_query()) {

    // カスタムフィールド 'show-message' の値を取得
    $show_message = get_post_meta(get_the_ID(), 'show-message', true);

    // 'show-message' の値が1の場合にのみメッセージを追加
    if ($show_message === '1') {
      $message = my_first_plugin_get_message();
      $style = my_first_plugin_get_message_style();
      $content .= '<div class="my-first-plugin-message" style="' . esc_attr($style) . '">' . wp_kses_post($message) . '</div>';
    }
  }
  return $content;
}
add_filter('the_content', 'my_first_plugin_add_message');

// 以降は変更なし

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_first_plugin_add_admin_menu() {
  add_options_page(
    __('My First Plugin Settings', 'my-first-plugin'),
    __('My First Plugin', 'my-first-plugin'),
    'manage_options',
    'my-first-plugin',
    'my_first_plugin_settings_page'
  );
}
add_action('admin_menu', 'my_first_plugin_add_admin_menu');

/**
* 設定ページの出力
*
*/
function my_first_plugin_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My First Plugin Settings', 'my-first-plugin'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_first_plugin_options');
      do_settings_sections('my-first-plugin');
      submit_button();
      ?>
    </form>
    <h2><?php esc_html_e('Live Preview', 'my-first-plugin'); ?></h2>
    <div id="my-first-plugin-preview-wrapper" style="padding: 10px; border: 1px solid #ccc; background: #fff;">
      <div id="my-first-plugin-preview" style="<?php echo esc_attr(my_first_plugin_get_message_style()); ?>">
        <?php echo wp_kses_post(my_first_plugin_get_message()); ?>
      </div>
    </div>
  </div>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      let textarea = document.querySelector('[name="my_first_plugin_message"]');
      let styleInput = document.querySelector('[name="my_first_plugin_message_style"]');
      let preview = document.getElementById('my-first-plugin-preview');
      textarea.addEventListener('input', function() {
        preview.innerHTML = textarea.value;
      });
      styleInput.addEventListener('input', function() {
        preview.style = styleInput.value;
      });
    });
  </script>
<?php
}

/**
* 設定項目の登録
*/
function my_first_plugin_settings_init() {
  register_setting('my_first_plugin_options', 'my_first_plugin_message', ['sanitize_callback' => 'wp_kses_post']);
  register_setting('my_first_plugin_options', 'my_first_plugin_message_style', ['sanitize_callback' => 'sanitize_text_field']);

  add_settings_section(
    'my_first_plugin_section',
    __('Message Settings', 'my-first-plugin'),
    function () {
      echo '<p>' . __('Customize the message', 'my-first-plugin') . '</p>';
    },
    'my-first-plugin'
  );

  add_settings_field(
    'my_first_plugin_message_textarea',
    __('Message to display', 'my-first-plugin'),
    'my_first_plugin_message_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );

  add_settings_field(
    'my_first_plugin_message_style',
    __('Message CSS Style', 'my-first-plugin'),
    'my_first_plugin_message_style_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );
}
add_action('admin_init', 'my_first_plugin_settings_init');

/**
* 設定フィールドの出力
*/
function my_first_plugin_message_field_callback() {
  $message = my_first_plugin_get_message();
?>
  <textarea name="my_first_plugin_message" rows="5" cols="50" class="large-text code"><?php echo esc_textarea($message); ?></textarea>
  <p class="description"><?php esc_html_e('You can use HTML in this message.', 'my-first-plugin'); ?></p>
<?php
}

function my_first_plugin_message_style_field_callback() {
  $style = my_first_plugin_get_message_style();
?>
  <input type="text" name="my_first_plugin_message_style" value="<?php echo esc_attr($style); ?>" class="large-text code">
  <p class="description"><?php esc_html_e('Enter custom CSS styles for the message (e.g., "color: red; font-size: 20px;").', 'my-first-plugin'); ?></p>
<?php
}

get_post_meta() は、投稿や固定ページに保存されたカスタムフィールドの値を取得する関数で、以下が基本構文です。

$value = get_post_meta($post_id, $meta_key, $single);
パラメータ 説明
$post_id メタデータを取得する投稿の ID。
$meta_key 取得したいカスタムフィールドのキー名。
$single true なら最初の値を取得、false(デフォルト)なら配列で取得。

カスタムフィールドを表示

編集画面でカスタムフィールドを表示するには、ページの設定でカスタムフィールドが有効になっている必要があります(デフォルトでは無効)。

「カスタムフィールドを追加」で「新規追加」をクリックします。

「名前」に「show-message」、「値」に「1」を入力して、「カスタムフィールドを追加」をクリックします。

カスタムフィールド show-message が設定されます。必要に応じて値を変更して「更新」したり、設定したカスタムフィールドを「削除」することができます。

また、カスタムフィールドは一度追加すると、他の記事でも次回以降はプルダウンから選択することができます。

メタボックスの使用

メタボックスを追加し、チェックボックスでメッセージの表示・非表示を切り替えられるようにします。

以下が変更後のコードです。投稿の最後にメッセージを追加する関数は同じです。

add_meta_box() を使い、投稿編集画面のサイドメニューにチェックボックスを追加し(48-58行目)、add_meta_box() の第3パラメータに指定したコールバック関数でフォームを出力します(65-78行目)。

その際、wp_nonce_field() を使ってセキュリティ対策用の nonce フィールドを設定します(72行目)。

そして、save_post フックを使い、チェックが入っていれば update_post_meta() で '1' を保存し、チェックが外れた場合は delete_post_meta() でデータを削除します(85-110行目)。

<?php

/**
* Plugin Name: My First Plugin
* Description: A plugin to show a customizable message at the end of posts.
* Version: 0.2.1
* Text Domain: my-first-plugin
*/

if (!defined('ABSPATH')) {
  exit;
}

function my_first_plugin_get_message() {
  return get_option('my_first_plugin_message') ? get_option('my_first_plugin_message') : 'Thank you for reading!';
}

function my_first_plugin_get_message_style() {
  return get_option('my_first_plugin_message_style') ? get_option('my_first_plugin_message_style') : 'color: #666; font-size: 16px;';
}

/**
* 投稿の最後にメッセージを追加するフィルター(フロントエンドへの出力)
*
* @param string $content 投稿コンテンツ
* @return string 変更後のコンテンツ
*/
function my_first_plugin_add_message($content) {
  if (is_single() && is_main_query()) {

    // 'show-message' メタデータを取得
    $show_message = get_post_meta(get_the_ID(), 'show-message', true);

    // チェックボックスがオン(value が 1)のときのみメッセージを表示
    if ($show_message === '1') {
      $message = my_first_plugin_get_message();
      $style = my_first_plugin_get_message_style();
      $content .= '<div class="my-first-plugin-message" style="' . esc_attr($style) . '">' . wp_kses_post($message) . '</div>';
    }
  }
  return $content;
}
add_filter('the_content', 'my_first_plugin_add_message');

/**
* メタボックスを追加
*/
function my_first_plugin_add_meta_box() {
  add_meta_box(
    'my_first_plugin_meta_box', // メタボックスの識別子
    __('Add Message', 'my-first-plugin'), // メタボックスのタイトル
    'my_first_plugin_meta_box_callback', // 表示用コールバック関数
    'post', // 表示する投稿タイプ
    'side', // 表示位置
    'default' // 優先度
  );
}
add_action('add_meta_boxes', 'my_first_plugin_add_meta_box');

/**
* メタボックスの表示(フォームを出力)
*
* @param WP_Post $post 現在の投稿オブジェクト
*/
function my_first_plugin_meta_box_callback($post) {
  // 保存されている値を取得
  $show_message = get_post_meta($post->ID, 'show-message', true);
  // $show_message === '1' の場合は input 要素の checked 属性を作成
  $checked = ($show_message === '1') ? 'checked' : '';

  // nonce フィールド(セキュリティ用)を出力
  wp_nonce_field('my_first_plugin_save_meta_box', 'my_first_plugin_nonce');

  echo '<label for="my_first_plugin_show_message">';
  echo '<input type="checkbox" id="my_first_plugin_show_message" name="my_first_plugin_show_message" value="1" ' . $checked . '>';
  echo __('Show Message', 'my-first-plugin');
  echo '</label>';
}

/**
* メタボックスの保存処理
*
* @param int $post_id 投稿ID
*/
function my_first_plugin_save_meta_box($post_id) {
  // 記事の自動保存時は処理しない
  if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
    return;
  }

  // nonce チェック
  if (!isset($_POST['my_first_plugin_nonce']) || !wp_verify_nonce($_POST['my_first_plugin_nonce'], 'my_first_plugin_save_meta_box')) {
    // nonce を検証し値が正しくなければ何もしない
    return;
  }

  // ユーザーの権限をチェック
  if (!current_user_can('edit_post', $post_id)) {
    // ユーザーが編集権限を持っていない場合は何もしない
    return;
  }

  // チェックボックスの値を取得(チェックがない場合は削除)
  if (isset($_POST['my_first_plugin_show_message'])) {
    update_post_meta($post_id, 'show-message', '1');
  } else {
    delete_post_meta($post_id, 'show-message');
  }
}
add_action('save_post', 'my_first_plugin_save_meta_box');

/**
* 以降はこれまでと同じ
*/

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_first_plugin_add_admin_menu() {
  add_options_page(
    __('My First Plugin Settings', 'my-first-plugin'),
    __('My First Plugin', 'my-first-plugin'),
    'manage_options',
    'my-first-plugin',
    'my_first_plugin_settings_page'
  );
}
add_action('admin_menu', 'my_first_plugin_add_admin_menu');

/**
* 設定ページの出力
*
*/
function my_first_plugin_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My First Plugin Settings', 'my-first-plugin'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_first_plugin_options');
      do_settings_sections('my-first-plugin');
      submit_button();
      ?>
    </form>
    <h2><?php esc_html_e('Live Preview', 'my-first-plugin'); ?></h2>
    <div id="my-first-plugin-preview-wrapper" style="padding: 10px; border: 1px solid #ccc; background: #fff;">
      <div id="my-first-plugin-preview" style="<?php echo esc_attr(my_first_plugin_get_message_style()); ?>">
        <?php echo wp_kses_post(my_first_plugin_get_message()); ?>
      </div>
    </div>
  </div>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      let textarea = document.querySelector('[name="my_first_plugin_message"]');
      let styleInput = document.querySelector('[name="my_first_plugin_message_style"]');
      let preview = document.getElementById('my-first-plugin-preview');
      textarea.addEventListener('input', function() {
        preview.innerHTML = textarea.value;
      });
      styleInput.addEventListener('input', function() {
        preview.style = styleInput.value;
      });
    });
  </script>
<?php
}

/**
* 設定項目の登録
*/
function my_first_plugin_settings_init() {
  register_setting('my_first_plugin_options', 'my_first_plugin_message', ['sanitize_callback' => 'wp_kses_post']);
  register_setting('my_first_plugin_options', 'my_first_plugin_message_style', ['sanitize_callback' => 'sanitize_text_field']);

  add_settings_section(
    'my_first_plugin_section',
    __('Message Settings', 'my-first-plugin'),
    function () {
      echo '<p>' . __('Customize the message', 'my-first-plugin') . '</p>';
    },
    'my-first-plugin'
  );

  add_settings_field(
    'my_first_plugin_message_textarea',
    __('Message to display', 'my-first-plugin'),
    'my_first_plugin_message_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );

  add_settings_field(
    'my_first_plugin_message_style',
    __('Message CSS Style', 'my-first-plugin'),
    'my_first_plugin_message_style_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );
}
add_action('admin_init', 'my_first_plugin_settings_init');

/**
* 設定フィールドの出力
*/
function my_first_plugin_message_field_callback() {
  $message = my_first_plugin_get_message();
?>
  <textarea name="my_first_plugin_message" rows="5" cols="50" class="large-text code"><?php echo esc_textarea($message); ?></textarea>
  <p class="description"><?php esc_html_e('You can use HTML in this message.', 'my-first-plugin'); ?></p>
<?php
}

function my_first_plugin_message_style_field_callback() {
  $style = my_first_plugin_get_message_style();
?>
  <input type="text" name="my_first_plugin_message_style" value="<?php echo esc_attr($style); ?>" class="large-text code">
  <p class="description"><?php esc_html_e('Enter custom CSS styles for the message (e.g., "color: red; font-size: 20px;").', 'my-first-plugin'); ?></p>
<?php
}

add_meta_box()

add_meta_box() は、WordPress の投稿編集画面に カスタムメタボックス(入力フィールドやチェックボックスなど)を追加する関数で、add_meta_boxes フックでメタボックスを追加します。

$callback 関数で 入力フィールドのHTMLを出力し、save_post フックでデータを保存します。

add_meta_box(
  $id,           // メタボックスの識別子
  $title,        // メタボックスのタイトル
  $callback,     // 表示用コールバック関数
  $screen,       // 表示する投稿タイプ 'post', 'page' など (初期値: null)
  $context,      // 表示位置  'normal', 'side', 'advanced'(初期値)
  $priority      // 優先度 'high', 'low', 'default'(初期値)
);

セキュリティ対策

CSRF対策 として、$callback 関数で入力フィールドのHTMLを出力する際は、wp_nonce_field() でノンス(nonce)の隠し要素を出力し、save_post フックでノンスを確認する必要があります。

  • wp_nonce_field( 'nonce_action', 'nonce_name' ) をメタボックス内で出力
  • save_postisset() を使いノンスが送信されているかチェック
  • wp_verify_nonce( $_POST['nonce_name'], 'nonce_action' ) でリクエストの正当性を検証
  • current_user_can('edit_post', $post_id) で 適切な権限があるか確認
  • 文字列を出力する場合は sanitize_text_field() で 入力値を安全に処理

HTMLを出力するコールバック関数の wp_nonce_field() により、hidden 属性が設定された nonce の値を持つ input 要素と隠しフィールドを生成するページの URL の情報を含む input 要素が出力されます。

<div class="inside">
  <input type="hidden" id="my_first_plugin_nonce" name="my_first_plugin_nonce" value="0abd3b0b15">
  <input type="hidden" name="_wp_http_referer" value="/wp-sample/wp-admin/post.php?post=1&action=edit">
  <label for="my_first_plugin_show_message">
    <input type="checkbox" id="my_first_plugin_show_message" name="my_first_plugin_show_message" value="1">
    Show Message</label>
</div>

上記のコードにより、以下のようなメッセージの表示・非表示を切り替えるチェックボックを持つメタボックスがサイドバーの下部に表示されます。

また、ページの設定を開くと追加したメタボックス(Add Message)の表示設定が追加されます。カスタムフィールドも同時に表示すると、紛らわしいため無効にします。

関連ページ:WordPress カスタムフィールドの使い方

テキスト入力を追加

グローバルなメッセージ「Thank you for reading!(デフォルトの場合)」に、投稿ごとにメッセージを追加できるように変更します。以下が変更点です。

  1. custom-message カスタムフィールドを追加
    • 投稿ごとにカスタムメッセージを保存・表示可能に。
  2. メタボックスにテキストエリアを追加
    • 投稿編集画面で HTML を含むメッセージを入力可能に。
  3. メタボックスの表示位置をコンテンツの下に変更
    • サイドバーでは狭いので投稿編集画面コンテンツの下にメタボックスを表示。
  4. wp_kses_post() で HTML を許可
    • WordPress で許可されている HTML タグのみ保存可能に。
  5. メッセージの追加条件
    • チェックボックスがオンのときのみ表示
    • custom-message に文字列があれば、Thank you メッセージの下に追加

以下が変更後のコードです。

テキストエリアで入力する追加のメッセージは HTML を許可しているので、出力する際に wp_kses_post() で投稿コンテンツに対して許可された HTML のみを許可しています。

<?php

/**
* Plugin Name: My First Plugin
* Description: A plugin to show a customizable message at the end of posts.
* Version: 0.2.2
* Text Domain: my-first-plugin
*/

if (!defined('ABSPATH')) {
  exit;
}

function my_first_plugin_get_message() {
  return get_option('my_first_plugin_message') ? get_option('my_first_plugin_message') : 'Thank you for reading!';
}

function my_first_plugin_get_message_style() {
  return get_option('my_first_plugin_message_style') ? get_option('my_first_plugin_message_style') : 'color: #666; font-size: 16px;';
}

/**
* 投稿の最後にメッセージを追加するフィルター(フロントエンドへの出力)
*
* @param string $content 投稿コンテンツ
* @return string 変更後のコンテンツ
*/
function my_first_plugin_add_message($content) {
  if (is_single() && is_main_query()) {

    $show_message = get_post_meta(get_the_ID(), 'show-message', true);
    // カスタムメッセージを取得
    $custom_message = get_post_meta(get_the_ID(), 'custom-message', true);

    if ($show_message === '1') {
      $message = my_first_plugin_get_message();
      $style = my_first_plugin_get_message_style();
      $message = '<div class="my-first-plugin-message" style="' . esc_attr($style) . '">' . wp_kses_post($message) . '</div>';

      // カスタムメッセージがある場合は追加
      if (!empty($custom_message)) {
        // wp_kses_post() で WordPress で許可されている HTML タグのみ保存
        $message .= '<div class="my-first-plugin-custom-message">' . wp_kses_post($custom_message) . '</div>';
      }

      $content .= $message;
    }
  }
  return $content;
}
add_filter('the_content', 'my_first_plugin_add_message');

/**
* メタボックスを追加(表示位置を変更)
*/
function my_first_plugin_add_meta_box() {
  add_meta_box(
    'my_first_plugin_meta_box',
    __('Add Message', 'my-first-plugin'),
    'my_first_plugin_meta_box_callback',
    'post',
    'normal', // サイドバーからコンテンツ下に変更
    'default'
  );
}
add_action('add_meta_boxes', 'my_first_plugin_add_meta_box');

/**
* メタボックスの表示(カスタムメッセージのテキストエリアを追加)
*
* @param WP_Post $post 現在の投稿オブジェクト
*/
function my_first_plugin_meta_box_callback($post) {

  $show_message = get_post_meta($post->ID, 'show-message', true);
  // 追加されたカスタムメッセージ
  $custom_message = get_post_meta($post->ID, 'custom-message', true);
  $checked = ($show_message === '1') ? 'checked' : '';

  wp_nonce_field('my_first_plugin_save_meta_box', 'my_first_plugin_nonce');

  echo '<p>';
  echo '<label for="my_first_plugin_show_message">';
  echo '<input type="checkbox" id="my_first_plugin_show_message" name="my_first_plugin_show_message" value="1" ' . $checked . '>';
  echo __(' Show Message', 'my-first-plugin');
  echo '</label>';
  echo '</p>';

  // カスタムメッセージのテキストエリアを追加
  echo '<p>';
  echo '<label for="my_first_plugin_custom_message">';
  echo __('Custom Message:', 'my-first-plugin');
  echo '</label>';
  echo '</p>';
  echo '<textarea id="my_first_plugin_custom_message" name="my_first_plugin_custom_message" rows="4" style="width:100%;">' . esc_textarea($custom_message) . '</textarea>';
}

/**
* メタボックスの保存処理(カスタムメッセージの保存を追加)
*
* @param int $post_id 投稿ID
*/
function my_first_plugin_save_meta_box($post_id) {
  if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
    return;
  }
  if (!isset($_POST['my_first_plugin_nonce']) || !wp_verify_nonce($_POST['my_first_plugin_nonce'], 'my_first_plugin_save_meta_box')) {
    return;
  }
  if (!current_user_can('edit_post', $post_id)) {
    return;
  }
  if (isset($_POST['my_first_plugin_show_message'])) {
    update_post_meta($post_id, 'show-message', '1');
  } else {
    delete_post_meta($post_id, 'show-message');
  }

  // カスタムメッセージの保存
  if (isset($_POST['my_first_plugin_custom_message'])) {
    update_post_meta($post_id, 'custom-message', wp_kses_post($_POST['my_first_plugin_custom_message']));
  } else {
    delete_post_meta($post_id, 'custom-message');
  }
}
add_action('save_post', 'my_first_plugin_save_meta_box');

/**
* 以降はこれまでと同じ
*/

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_first_plugin_add_admin_menu() {
  add_options_page(
    __('My First Plugin Settings', 'my-first-plugin'),
    __('My First Plugin', 'my-first-plugin'),
    'manage_options',
    'my-first-plugin',
    'my_first_plugin_settings_page'
  );
}
add_action('admin_menu', 'my_first_plugin_add_admin_menu');

/**
* 設定ページの出力
*
*/
function my_first_plugin_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My First Plugin Settings', 'my-first-plugin'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_first_plugin_options');
      do_settings_sections('my-first-plugin');
      submit_button();
      ?>
    </form>
    <h2><?php esc_html_e('Live Preview', 'my-first-plugin'); ?></h2>
    <div id="my-first-plugin-preview-wrapper" style="padding: 10px; border: 1px solid #ccc; background: #fff;">
      <div id="my-first-plugin-preview" style="<?php echo esc_attr(my_first_plugin_get_message_style()); ?>">
        <?php echo wp_kses_post(my_first_plugin_get_message()); ?>
      </div>
    </div>
  </div>
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      let textarea = document.querySelector('[name="my_first_plugin_message"]');
      let styleInput = document.querySelector('[name="my_first_plugin_message_style"]');
      let preview = document.getElementById('my-first-plugin-preview');
      textarea.addEventListener('input', function() {
        preview.innerHTML = textarea.value;
      });
      styleInput.addEventListener('input', function() {
        preview.style = styleInput.value;
      });
    });
  </script>
<?php
}

/**
* 設定項目の登録
*/
function my_first_plugin_settings_init() {
  register_setting('my_first_plugin_options', 'my_first_plugin_message', ['sanitize_callback' => 'wp_kses_post']);
  register_setting('my_first_plugin_options', 'my_first_plugin_message_style', ['sanitize_callback' => 'sanitize_text_field']);

  add_settings_section(
    'my_first_plugin_section',
    __('Message Settings', 'my-first-plugin'),
    function () {
      echo '<p>' . __('Customize the message', 'my-first-plugin') . '</p>';
    },
    'my-first-plugin'
  );

  add_settings_field(
    'my_first_plugin_message_textarea',
    __('Message to display', 'my-first-plugin'),
    'my_first_plugin_message_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );

  add_settings_field(
    'my_first_plugin_message_style',
    __('Message CSS Style', 'my-first-plugin'),
    'my_first_plugin_message_style_field_callback',
    'my-first-plugin',
    'my_first_plugin_section'
  );
}
add_action('admin_init', 'my_first_plugin_settings_init');

/**
* 設定フィールドの出力
*/
function my_first_plugin_message_field_callback() {
  $message = my_first_plugin_get_message();
?>
  <textarea name="my_first_plugin_message" rows="5" cols="50" class="large-text code"><?php echo esc_textarea($message); ?></textarea>
  <p class="description"><?php esc_html_e('You can use HTML in this message.', 'my-first-plugin'); ?></p>
<?php
}

function my_first_plugin_message_style_field_callback() {
  $style = my_first_plugin_get_message_style();
?>
  <input type="text" name="my_first_plugin_message_style" value="<?php echo esc_attr($style); ?>" class="large-text code">
  <p class="description"><?php esc_html_e('Enter custom CSS styles for the message (e.g., "color: red; font-size: 20px;").', 'my-first-plugin'); ?></p>
<?php
}

編集画面の下部に以下のようにメタボックスが表示され、テキストエリアにメッセージを入力できます。

HTML を入力できるので、リンクなどを設定することができます。

Plugin Handbook: Custom Meta Boxes

CSS や JavaScript の読み込み

プラグインから CSS や JavaScript ファイルを読み込む方法です。

この例では、前述のプラグイン(My First Plugin)のフォルダ内に「assets」フォルダを作成し、その中に CSS と JavaScript ファイルを配置します。ファイル名は任意の名前を付けられます。

my-first-plugin/
  ├── assets/
  │   ├── my-first-plugin-admin.css (設定ページ用 CSS)
  │   ├── my-first-plugin-admin.js (設定ページ用 JavaScript)
  │   ├── my-first-plugin.css (フロントエンド用 CSS)
  │   └── my-first-plugin.js (フロントエンド用 JavaScript)
  └── my-first-plugin.php (プラグインファイル)s

例えば、以下のような CSS と JavaScript を用意します(確認用なので内容は意味がありません)。

h1 {
  background-color: aquamarine;
}
console.log('my first plugin admin page!');
.my-first-plugin-message {
  border: 1px solid #ccc;
  padding: 0.5rem;
}
console.log('my first plugin front end!');

プラグインディレクトリの URL や パス

以下はプラグインディレクトリの URL や パスを取得する関数と定数です。

関数 説明 返す値の例
plugin_dir_url(__FILE__) プラグインのディレクトリURLを取得 http://example.com/wp-content/plugins/my-plugin/
plugins_url('path/to/file', __FILE__) プラグインフォルダ内の特定のファイルのURLを取得 http://example.com/wp-content/plugins/my-plugin/path/to/file
plugin_dir_path(__FILE__) プラグインのディレクトリパス(ローカル)を取得 /var/www/html/wp-content/plugins/my-plugin/
WP_PLUGIN_URL プラグインディレクトリのURL(定数) http://example.com/wp-content/plugins
WP_PLUGIN_DIR プラグインディレクトリの絶対パス(定数) /var/www/html/wp-content/plugins

__FILE__ は読み込まれたファイルのパスとファイル名を表す PHP の定数です。

関連ページ:WordPress CSSやJavaScriptファイルの読み込み

フロントエンドにエンキュー

フックは、wp_enqueue_scripts アクションフックを使用します。

以下はプラグインファイルで、上記の CSS と JavaScript をフロントエンドに読み込む例です。

// wp_enqueue_scripts アクションフックを使用
add_action('wp_enqueue_scripts', 'my_first_plugin_enqueue_scripts');

function my_first_plugin_enqueue_scripts() {
  wp_enqueue_style(
    'my-first-plugin-style',
    plugin_dir_url(__FILE__). 'assets/my-first-plugin.css',
    array(),
    filemtime(plugin_dir_path(__FILE__) . 'assets/my-first-plugin.css')
  );
  wp_enqueue_script(
    'my-first-plugin-script',
    plugin_dir_url(__FILE__). 'assets/my-first-plugin.js',
    array(),
    filemtime(plugin_dir_path(__FILE__) . 'assets/my-first-plugin.js'),
    true
  );
}

または、以下のように変数を使って記述することもできます(上記と同じこと)。

add_action('wp_enqueue_scripts', 'my_first_plugin_enqueue_scripts');

function my_first_plugin_enqueue_scripts() {
  $plugin_dir = plugin_dir_path(__FILE__);
  $plugin_url = plugin_dir_url(__FILE__);
  $css_file   = 'assets/my-first-plugin.css';
  $script_file   = 'assets/my-first-plugin.js';

  wp_enqueue_style(
    'my-first-plugin-style',
    $plugin_url . $css_file,
    array(),
    filemtime($plugin_dir . $css_file)
  );
  wp_enqueue_script(
    'my-first-plugin-script',
    $plugin_url . $script_file,
    array(),
    filemtime($plugin_dir . $script_file),
    true
  );
}
選択的に読み込む

上記の例の場合、CSS と JavaScript はサイトのすべてのページでキューに入れられます。

前述のプラグインの関数の処理では、is_single() と is_main_query() でメインクエリの投稿ページのみ対象しているので、CSS と JavaScript の読み込みも以下のようにメインクエリの投稿ページのみ対象します。

function my_first_plugin_enqueue_scripts() {

  // メインクエリの投稿ページの場合にのみエンキュー
  if (is_single() && is_main_query()) {
    $plugin_dir = plugin_dir_path(__FILE__);
    $plugin_url = plugin_dir_url(__FILE__);
    $css_file   = 'assets/my-first-plugin.css';
    $script_file   = 'assets/my-first-plugin.js';
    wp_enqueue_style(
      'my-first-plugin-style',
      $plugin_url . $css_file,
      array(),
      filemtime($plugin_dir . $css_file)
    );
    wp_enqueue_script(
      'my-first-plugin-script',
      $plugin_url . $script_file,
      array(),
      filemtime($plugin_dir . $script_file),
      true
    );
  }
}

CSS と JavaScript が読み込まれると、例えば以下のようにメッセージに枠線が表示され、コンソールに「my first plugin front end!」と出力されます。

管理画面にエンキュー

管理画面(カスタム設定ページなど)で CSS や JavaScript をエンキューするには、admin_enqueue_scripts アクションフックを使用します。

以下はプラグインのカスタム設定ページでのみ、CSS と JavaScript を読み込む例です。

$hook は、現在表示されている管理ページの識別子(スラッグ)を表します。

プラグインの設定ページのスラッグ settings_page_my-first-plugin は add_options_page() で追加した設定ページの識別子(my-first-plugin)にプリフィックス settings_page_ を付けたものになります。

$allowed_pages に読み込む対象のページのスラッグの配列を指定します(この場合は1つだけ)。そして in_array($hook, $allowed_pages) で読み込むべきページか判定しています。

もし、すべての管理画面で読み込む場合は $hook のチェックを省略します。

この例の場合、add_options_page() で設定ページを追加したので、設定ページのスラッグには settings_page_ というプリフィックスが付きますが、add_menu_page() で設定ページを追加した場合は、toplevel_page_ というプリフィックスが付くなど使用する関数により異なります。

// admin_enqueue_scripts アクションフックを使用
add_action('admin_enqueue_scripts', 'my_first_plugin_admin_styles');

function my_first_plugin_admin_styles($hook) {

  // プラグインの設定ページのスラッグ
  $allowed_pages = ['settings_page_my-first-plugin'];

  // $hook が対象のページであれば
  if (in_array($hook, $allowed_pages)) {
    $plugin_dir = plugin_dir_path(__FILE__);
    $plugin_url = plugin_dir_url(__FILE__);
    $css_file   = 'assets/my-first-plugin-admin.css';
    $script_file   = 'assets/my-first-plugin-admin.js';
    wp_enqueue_style(
      'my-first-plugin-admin-style',
      $plugin_url . $css_file,
      array(),
      filemtime($plugin_dir . $css_file)
    );
    wp_enqueue_script(
      'my-first-plugin-admin-script',
      $plugin_url . $script_file,
      array(),
      filemtime($plugin_dir . $script_file),
      true
    );
  }
}

CSS と JavaScript が読み込まれると、例えば以下のように h1 要素に背景色が適用され、コンソールに「my first plugin admin page!」と出力されます。

管理画面のスラッグを調べる方法

$hook の値を確認したい場合、以下のコードを実行すれば、現在のページの $hook 値を確認できます。

add_action('admin_enqueue_scripts', function($hook) {
  error_log('Admin page hook: ' . $hook);
});

エラーログ (wp-content/debug.log) にフック名が出力されるので確認します。

但し、wp-content/debug.log は、デバッグモードが有効になっていて、ログを保存する設定が有効になっている場合にのみ生成されます。

具体的には、wp-config.php に以下が設定されている必要があります。

define('WP_DEBUG', true);  // デバッグモードを有効化
define('WP_DEBUG_LOG', true);  // ログを wp-content/debug.log に保存

本番環境では(不要になったら)、wp-config.php のデバッグ設定を無効にします。

define('WP_DEBUG', false);  // デバッグモードを無効化
define('WP_DEBUG_LOG', false);  // ログを wp-content/debug.log に保存しない

または、以下を記述すると管理画面の右上にスラッグが表示されます(確認したら忘れずにに削除します)。

add_action('admin_enqueue_scripts', function($hook) {
  echo '<h3 style="float:right; margin-right:30px;">スラッグ: '. $hook .'</h3>';
});

カスタム投稿タイプとカスタムタクソノミー

カスタム投稿タイプとカスタムタクソノミーをプラグインで登録する例です。

カスタム投稿タイプやカスタムタクソノミーはテーマの functions.php で登録することもできますが、プラグインで作成することが推奨されています(テーマが変更されてもコンテンツが移植可能になります)。

プラグインディレクトリに my-custom-post-types というディレクトリを作成し、その中に my-custom-post-types.php というメインのプラグインファイルを作成します。

plugins/
  └── my-custom-post-types/
      └── my-custom-post-types.php

カスタム投稿タイプは register_post_type() を使い、カスタムタクソノミーは register_taxonomy() を使って init アクションで登録します。

登録自体は単に register_post_type() や register_taxonomy() を呼び出すだけですが、設定項目(パラメータ)が多いので、以下ではカスタム投稿タイプとカスタムタクソノミーを登録する独自の関数を定義し、init アクションの中で呼び出すようにしています。

<?php

/**
* Plugin Name: My Custom Post Types
* Description: A plugin to create custom post types
* Version: 0.1.0
*
*/

if (! defined('ABSPATH')) {
  exit;
}

/**
 * Registers custom post types and taxonomies on the 'init' action.
 */
add_action('init', function () {
  // News カスタム投稿タイプを登録
  my_custom_post_types_register_post_type('News', 'News');
  // News Categories カスタムタクソノミーを登録
  my_custom_post_types_register_taxonomy('news', 'News Categories', 'News Category');
  // News Tags カスタムタクソノミーを登録
  my_custom_post_types_register_taxonomy('news', 'News Tags', 'News Tag', false);
  // 必要に応じて上記のように関数を呼び出して、複数のカスタム投稿タイプやカスタムタクソノミーを登録可能
});

/**
* Generates a slug from a given name.
*
* @param string $name The name to convert into a slug.
* @return string The generated slug.
*/
function create_slug($name) {
  return sanitize_title($name);
}

/**
* Registers a custom post type.
*
* @param string      $name          The plural label for the post type.
* @param string      $singular_name The singular label for the post type.
* @param string|null $post_type     The custom post type slug (auto-generated if null).
* @param string      $menu_icon     The Dashicons class for the menu icon.
*/
function my_custom_post_types_register_post_type($name, $singular_name, $post_type = null, $menu_icon = 'dashicons-admin-post') {

  if ($post_type === null) {
    $post_type = create_slug($singular_name);
  }

  $args = array(
    'labels'       => array(
      'name' => $name,
      'singular_name' => $name,
      'menu_name' => $name,
      'all_items' => $name . ' 一覧',
      'edit_item' => $name . ' を編集',
      'view_item' => $name . ' を表示',
      'view_items' => $name . ' を表示',
      'add_new_item' => '新規 ' . $name . ' を追加',
      'add_new' => '新規 ' . $name . ' を追加',
      'new_item' => '新規 ' . $name,
      'parent_item_colon' => '親の ' . $name . ' :',
      'search_items' =>  $name . ' を検索',
      'not_found' => $post_type . ' が見つかりませんでした。',
      'not_found_in_trash' => 'ゴミ箱に ' . $post_type . ' はありません',
      'archives' => $name . ' アーカイブ',
      'attributes' => $name . ' の属性',
      'insert_into_item' => $post_type . ' に挿入',
      'uploaded_to_this_item' => 'この ' . $post_type . ' にアップロードされました',
      'filter_items_list' => $post_type . ' リストを絞り込み',
      'filter_by_date' => $post_type . ' 日時で絞り込み',
      'items_list_navigation' => $name . ' list navigation',
      'items_list' => $name . ' リスト',
      'item_published' => $name . ' を公開しました。',
      'item_published_privately' => $name . ' published privately.',
      'item_reverted_to_draft' => $name . ' を下書きに戻しました。',
      'item_scheduled' => $name . ' を予約しました。',
      'item_updated' => $name . ' を更新しました。',
      'item_link' => $name . ' リンク',
      'item_link_description' => $post_type . ' へのリンク。',
    ),
    'public' => true,
    'show_in_rest' => true,
    'menu_icon' => $menu_icon,
    'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
    'has_archive' => true,
    'rewrite' => array(
      'slug'       => $post_type,
      'with_front' => false,
      'feeds'      => true,
      'pages'      => true,
    ),
  );

  register_post_type($post_type, $args);
}

/**
* Registers a custom taxonomy.
*
* @param string      $post_type              The post type to associate the taxonomy with.
* @param string      $taxonomy_name          The plural label for the taxonomy.
* @param string      $taxonomy_singular_name The singular label for the taxonomy.
* @param bool        $hierarchical           Whether the taxonomy is hierarchical (default: true).
* @param string|null $taxonomy_slug          The custom taxonomy slug (auto-generated if null).
*/
function my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical = true, $taxonomy_slug = null) {

  if ($taxonomy_slug === null) {
    $taxonomy_slug = create_slug($taxonomy_singular_name);
  }

  $args = array(
    'labels' => array(
      'name' => $taxonomy_name,
      'singular_name' => $taxonomy_singular_name,
      'menu_name' => $taxonomy_name,
      'all_items' => $taxonomy_name . ' 一覧',
      'edit_item' => $taxonomy_singular_name . ' を編集',
      'view_item' => $taxonomy_singular_name . ' を表示',
      'update_item' => $taxonomy_singular_name . ' を更新',
      'add_new_item' => '新規 ' . $taxonomy_singular_name . ' を追加',
      'new_item_name' => '新規 ' . $taxonomy_singular_name . ' 名',
      'parent_item' => '親 ' . $taxonomy_singular_name,
      'parent_item_colon' => '親 ' . $taxonomy_singular_name . ' :',
      'search_items' => $taxonomy_name . ' を検索',
      'not_found' => $taxonomy_singular_name . ' が見つかりませんでした。',
      'no_terms' => 'No ' . $taxonomy_name,
      'filter_by_item' => $taxonomy_singular_name . ' で絞り込む',
      'items_list_navigation' => $taxonomy_name . ' リストナビゲーション',
      'items_list' => $taxonomy_name . ' リスト',
      'back_to_items' => $taxonomy_name . '←  へ戻る',
      'item_link' => $taxonomy_singular_name . ' リンク',
      'item_link_description' =>  $taxonomy_singular_name . ' へのリンク',
    ),
    'public' => true,
    'show_ui' => true,
    'hierarchical' => $hierarchical,
    'show_in_rest' => true,
    'rewrite' => array('slug' => $taxonomy_slug),
  );

  register_taxonomy($taxonomy_slug, $post_type, $args);
}

create_slug()

sanitize_title() を使って、与えられた文字列をスラッグに適した形式に変換します。

例えば、「My Custom Post Type」なら「my-custom-post-type」に変換します(空白はハイフンに、大文字は小文字に変換されます)。

必要に応じてスラッグにプリフィックスを追加するなども考えられますが、この例の場合は、単に sanitize_title() を呼び出しているだけなので、この関数を定義せずに直接 sanitize_title() を呼び出しても同じです。

my_custom_post_types_register_post_type()

カスタム投稿タイプを登録する関数 my_custom_post_types_register_post_type() は以下の引数を受け取ります。

  • $name:カスタム投稿タイプ名(複数形)
  • $singular_name : カスタム投稿タイプ名(単数形)
  • $post_type :カスタム投稿タイプのスラッグ(省略可能)
  • $menu_icon :メニューに表示するアイコン(デフォルトは 'dashicons-admin-post')

$post_type を省略した場合は $singular_name の値を元に create_slug() でスラッグを生成します。

my_custom_post_types_register_taxonomy()

カスタムタクソノミーを登録する関数 my_custom_post_types_register_post_type() は以下の引数を受け取ります。

  • $post_type:投稿タイプ名。
  • $taxonomy_name:カスタムタクソノミー名(複数形)
  • $taxonomy_singular_name:カスタムタクソノミー名(単数形)
  • $hierarchical :親子関係(階層)を持たせるかどうかの真偽値。デフォルトは true
  • $taxonomy_slug :カスタムタクソノミーのスラッグ。省略した場合は $taxonomy_singular_name の値を元に生成

上記のコードの場合、以下のように News というカスタム投稿タイプと News Categories、News Tags の2つのカスタムタクソノミーが登録されます。

関連ページ:WordPress カスタム投稿タイプとカスタム分類

国際化対応

以下は上記を翻訳可能にしたバージョンです。

プラグインヘッダーに Text Domain を追加し、ラベルの出力は翻訳関数を使用しています。

ラベル部分の翻訳では、翻訳しやすくするため、sprintf() を使って変数の値を %s プレースホルダーに置き換え、%s プレースホルダーの意味を明確にする(翻訳者が %s の意味を理解しやすくする)ように、translators: コメントを追加しています。

また、"All %s" など投稿タイプとタクソノミーで同じ内容の翻訳があるので、_x( ) 関数を使ってコンテキストを指定することで、同じ文言でも異なる用途として区別するようにしています。

<?php

/**
* Plugin Name: My Custom Post Types
* Description: A plugin to create custom post types
* Version: 0.1.1
* Text Domain: my-custom-post-types
*
*/

if (! defined('ABSPATH')) {
  exit;
}

/**
* Registers custom post types and taxonomies on the 'init' action.
*/
add_action('init', function () {
  my_custom_post_types_register_post_type('News', 'News');
  my_custom_post_types_register_taxonomy('news', 'News Categories', 'News Category');
  my_custom_post_types_register_taxonomy('news', 'News Tags', 'News Tag', false);
});

/**
* Generates a slug from a given name.
*
* @param string $name The name to convert into a slug.
* @return string The generated slug.
*/
function create_slug($name) {
  return sanitize_title($name);
}

/**
* Registers a custom post type.
*
* @param string      $name          The plural label for the post type.
* @param string      $singular_name The singular label for the post type.
* @param string|null $post_type     The custom post type slug (auto-generated if null).
* @param string      $menu_icon     The Dashicons class for the menu icon.
*/
function my_custom_post_types_register_post_type($name, $singular_name, $post_type = null, $menu_icon = 'dashicons-admin-post') {

  if ($post_type === null) {
    $post_type = create_slug($singular_name);
  }

  $args = array(
    'labels'       => array(
      'name'                  => __($name, 'my-custom-post-types'),
      'singular_name'         => __($singular_name, 'my-custom-post-types'),
      'menu_name'             => __($name, 'my-custom-post-types'),
      // translators: %s is the post type name.
      'all_items'             => sprintf(_x('All %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the singular post type name.
      'view_item'             => sprintf(_x('View %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'view_items'            => sprintf(_x('View %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      'add_new'               => __('Add New', 'my-custom-post-types'),
      // translators: %s is the singular post type name.
      'new_item'              => sprintf(_x('New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'search_items'          => sprintf(_x('Search %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'not_found'             => sprintf(_x('No %s found.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'not_found_in_trash'    => sprintf(_x('No %s found in Trash.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'archives'              => sprintf(_x('%s Archives', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'attributes'            => sprintf(_x('%s Attributes', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'insert_into_item'      => sprintf(_x('Insert into %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'uploaded_to_this_item' => sprintf(_x('Uploaded to this %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_items_list'     => sprintf(_x('Filter %s list', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_by_date'        => sprintf(_x('Filter %s by date', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'items_list'            => sprintf(_x('%s list', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published'        => sprintf(_x('%s published.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published_privately' => sprintf(_x('%s published privately.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_reverted_to_draft' => sprintf(_x('%s reverted to draft.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_scheduled'        => sprintf(_x('%s scheduled.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_updated'          => sprintf(_x('%s updated.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_link'             => sprintf(_x('%s link', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'item_link_description' => sprintf(_x('A link to %s', 'post type slug', 'my-custom-post-types'), $post_type),
    ),
    'public' => true,
    'show_in_rest' => true,
    'menu_icon' => $menu_icon,
    'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
    'has_archive' => true,
    'rewrite' => array(
      'slug'       => $post_type,
      'with_front' => false,
      'feeds'      => true,
      'pages'      => true,
    ),
  );

  register_post_type($post_type, $args);
}

/**
* Registers a custom taxonomy.
*
* @param string      $post_type              The post type to associate the taxonomy with.
* @param string      $taxonomy_name          The plural label for the taxonomy.
* @param string      $taxonomy_singular_name The singular label for the taxonomy.
* @param bool        $hierarchical           Whether the taxonomy is hierarchical (default: true).
* @param string|null $taxonomy_slug          The custom taxonomy slug (auto-generated if null).
*/
function my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical = true, $taxonomy_slug = null) {

  if ($taxonomy_slug === null) {
    $taxonomy_slug = create_slug($taxonomy_singular_name);
  }

  $args = array(
    'labels' => array(
      'name'                  => __($taxonomy_name, 'my-custom-post-types'),
      'singular_name'         => __($taxonomy_singular_name, 'my-custom-post-types'),
      'menu_name'             => __($taxonomy_name, 'my-custom-post-types'),
      // translators: %s is the taxonomy name.
      'all_items'             => sprintf(_x('All %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'view_item'             => sprintf(_x('View %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'update_item'           => sprintf(_x('Update %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'new_item_name'         => sprintf(_x('New %s Name', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item'           => sprintf(_x('Parent %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'search_items'          => sprintf(_x('Search %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'not_found'             => sprintf(_x('No %s found.', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'no_terms'              => sprintf(_x('No %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'filter_by_item'        => sprintf(_x('Filter by %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'items_list'            => sprintf(_x('%s list', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'back_to_items'         => sprintf(_x('← Back to %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'item_link'             => sprintf(_x('%s link', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'item_link_description' => sprintf(_x('A link to %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
    ),
    'public' => true,
    'show_ui' => true,
    'hierarchical' => $hierarchical,
    'show_in_rest' => true,
    'rewrite' => array('slug' => $taxonomy_slug),
  );

  register_taxonomy($taxonomy_slug, $post_type, $args);
}

カスタム設定ページを追加

以下は、前項のプラグインに設定ページを追加して、設定ページでカスタム投稿タイプ名やカスタムタクソノミー名を設定できるようにしたものです(実用的なものではありません)。

「設定」メニューに「CPT Settings」が追加され、クリックすると以下のようなカスタム設定ページが表示さ、カスタム投稿タイプ名やカスタムタクソノミー名、その他のオプションを指定することができます。

※ 単にカスタム投稿タイプやカスタムタクソノミーを作成する場合は、Advanced Custom Fields (ACF) などの既成のプラグインを使うのが良いかと思います。

以下が変更後のコードです。

この例では、シンプルにフォームを作成するため、設定を register_setting() で1つの配列として登録しています。

設定を保存するためのオプション名を変数に保存し、admin_menu フックで add_options_page() を使って設定ページを追加します(19-27行目)。

そして add_options_page() に指定したコールバック関数 my_cpt_settings_page() で設定ページの HTML を出力します(30-91行目)。その際、 settings_fields() を使って適切な nonce フィールドや非表示の入力 (hidden input) を生成します。

admin_init フックで register_setting() を使って設定(配列)を登録(95行目)し、register_setting() のオプションの引数に指定したサニタイズコールバック関数を定義します(99-116行目)。

サニタイズコールバックでは、入力がなかった場合の設定(配列)のデフォルト値を設定し、チェックボックスが未チェック時に 0 を設定しています。

チェックボックスは、チェックされたときしかデータが送信されないため、チェックを外した場合 taxonomy_hierarchical は $input に含まれません。そのため、array_merge() を使ってデフォルト値 0 を設定し、未チェック時でも taxonomy_hierarchical = 0 になるようにしています(114行目)。

カスタム投稿タイプとカスタムタクソノミーを登録する処理(関数)は前述の例と同じです。

そして最後に init フックを使って、カスタム投稿タイプやタクソノミーを登録します。

その際、無名関数の中で $option_name を使えるようにするため use () を使っています。use ($option_name) は 無名関数の中で $option_name を使えるようにするための仕組みです。

その他の詳細はコメントを御覧ください。

<?php

/**
* Plugin Name: My Custom Post Types
* Description: A plugin to create custom post types
* Version: 0.2.0
* Text Domain: my-custom-post-types
*
*/

if (! defined('ABSPATH')) {
  exit;
}

// カスタム投稿タイプの設定を保存するための WordPress オプション名を変数に格納
$option_name = 'my_cpt_settings';

// 設定ページの追加
add_action('admin_menu', function () {
  add_options_page(
    __('Custom Post Type Settings', 'my-custom-post-types'),
    __('CPT Settings', 'my-custom-post-types'),
    'manage_options',
    'my-cpt-settings',
    'my_cpt_settings_page'
  );
});

// 設定ページの表示(HTMLを出力)
function my_cpt_settings_page() {
  // 関数の外で定義された変数 $option_name を関数内で使用するために global を宣言
  global $option_name;
  // オプションテーブル(wp_options)から $option_name というキーの設定データを取得。 [](空の配列)は、設定が存在しない場合のデフォルト値
  $options = get_option($option_name, []);
  // get_option() はデータを返すが、それが配列とは限らないので配列でなければ、空の配列 [] に置き換え
  if (!is_array($options)) {
    $options = [];
  }

  // 各設定値を取得
  $post_type_name = isset($options['post_type_name']) ? $options['post_type_name'] : '';
  $post_type_singular_name = isset($options['post_type_singular_name']) ? $options['post_type_singular_name'] : '';
  $post_type_slug = isset($options['post_type_slug']) ? $options['post_type_slug'] : '';
  $menu_icon = isset($options['menu_icon']) ? $options['menu_icon'] : 'dashicons-admin-post';
  $taxonomy_name = isset($options['taxonomy_name']) ? $options['taxonomy_name'] : '';
  $taxonomy_singular_name = isset($options['taxonomy_singular_name']) ? $options['taxonomy_singular_name'] : '';
  // taxonomy_hierarchical が空でないかをチェック。空でなければ 1(階層あり)、空なら 0(階層なし)を $taxonomy_hierarchical に設定
  $taxonomy_hierarchical = !empty($options['taxonomy_hierarchical']) ? 1 : 0;

?>
  <div class="wrap">
    <h2><?php esc_html_e('Custom Post Type Settings', 'my-custom-post-types'); ?></h2>
    <form method="post" action="options.php">
      <?php settings_fields('my_cpt_settings_group'); ?>
      <table class="form-table">
        <tr>
          <th><label for="post_type_name"><?php _e('Post Type Name (Plural)', 'my-custom-post-types'); ?></label></th>
          <td><input type="text" name="my_cpt_settings[post_type_name]" value="<?php echo esc_attr($post_type_name); ?>"></td>
        </tr>
        <tr>
          <th><label for="post_type_singular_name"><?php _e('Post Type Name (Singular)', 'my-custom-post-types'); ?></label></th>
          <td><input type="text" name="my_cpt_settings[post_type_singular_name]" value="<?php echo esc_attr($post_type_singular_name); ?>"></td>
        </tr>
        <tr>
          <th><label for="post_type_slug"><?php _e('Post Type Slug', 'my-custom-post-types'); ?></label></th>
          <td><input type="text" name="my_cpt_settings[post_type_slug]" value="<?php echo esc_attr($post_type_slug); ?>"></td>
        </tr>
        <tr>
          <th><label for="menu_icon"><?php _e('Menu Icon (Dashicons Class)', 'my-custom-post-types'); ?></label></th>
          <td><input type="text" name="my_cpt_settings[menu_icon]" value="<?php echo esc_attr($menu_icon); ?>"></td>
        </tr>
        <tr>
          <th><label for="taxonomy_name"><?php _e('Taxonomy Name (Plural)', 'my-custom-post-types'); ?></label></th>
          <td><input type="text" name="my_cpt_settings[taxonomy_name]" value="<?php echo esc_attr($taxonomy_name); ?>"></td>
        </tr>
        <tr>
          <th><label for="taxonomy_singular_name"><?php _e('Taxonomy Name (Singular)', 'my-custom-post-types'); ?></label></th>
          <td><input type="text" name="my_cpt_settings[taxonomy_singular_name]" value="<?php echo esc_attr($taxonomy_singular_name); ?>"></td>
        </tr>
        <tr>
          <th><label for="taxonomy_hierarchical"><?php _e('Taxonomy Hierarchical', 'my-custom-post-types'); ?></label></th>
          <td>
            <input type="checkbox" name="my_cpt_settings[taxonomy_hierarchical]" value="1" <?php checked(1, (int) $taxonomy_hierarchical); ?>><?php _e('Enable hierarchy', 'my-custom-post-types'); ?>
          </td>
        </tr>
      </table>
      <?php submit_button(); ?>
    </form>
  </div>
<?php
}

// 設定を登録(設定は配列)
add_action('admin_init', function () use ($option_name)  {
  register_setting('my_cpt_settings_group', $option_name, ['sanitize_callback' => 'my_cpt_settings_sanitize'] );
});

// register_setting() のサニタイズコールバックを定義
function my_cpt_settings_sanitize($input) {
  // $input が配列でない場合は、空の配列 [] に変換
  if (!is_array($input)) {
    $input = [];
  }

  // array_merge() を使って、デフォルト値の配列と送信されたデータ $input を結合してデフォルト値を設定(後に指定された値が、 前の値を上書き)
  return array_merge([
    'post_type_name' => '',
    'post_type_singular_name' => '',
    'post_type_slug' => '',
    'menu_icon' => 'dashicons-admin-post',
    'taxonomy_name' => '',
    'taxonomy_singular_name' => '',
    // チェックを外した場合 taxonomy_hierarchical は $input に含まれないので、array_merge() を使ってデフォルト値 0 を設定(未チェック時でも taxonomy_hierarchical = 0 になる)
    'taxonomy_hierarchical' => 0, // 未チェック時に 0
  ], $input);
}

/**
* Generates a slug from a given name.
*
* @param string $name The name to convert into a slug.
* @return string The generated slug.
*/
function create_slug($name) {
  return sanitize_title($name);
}

/**
* Registers a custom post type.
*
* @param string      $name          The plural label for the post type.
* @param string      $singular_name The singular label for the post type.
* @param string|null $post_type     The custom post type slug (auto-generated if null).
* @param string      $menu_icon     The Dashicons class for the menu icon.
*/
function my_custom_post_types_register_post_type($name, $singular_name, $post_type, $menu_icon) {

  if ($post_type === null) {
    $post_type = create_slug($singular_name);
  }

  if ($menu_icon === null) {
    $menu_icon = 'dashicons-admin-post';
  }

  $args = array(
    'labels'       => array(
      'name'                  => __($name, 'my-custom-post-types'),
      'singular_name'         => __($singular_name, 'my-custom-post-types'),
      'menu_name'             => __($name, 'my-custom-post-types'),
      // translators: %s is the post type name.
      'all_items'             => sprintf(_x('All %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the singular post type name.
      'view_item'             => sprintf(_x('View %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'view_items'            => sprintf(_x('View %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      'add_new'               => __('Add New', 'my-custom-post-types'),
      // translators: %s is the singular post type name.
      'new_item'              => sprintf(_x('New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'search_items'          => sprintf(_x('Search %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'not_found'             => sprintf(_x('No %s found.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'not_found_in_trash'    => sprintf(_x('No %s found in Trash.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'archives'              => sprintf(_x('%s Archives', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'attributes'            => sprintf(_x('%s Attributes', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'insert_into_item'      => sprintf(_x('Insert into %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'uploaded_to_this_item' => sprintf(_x('Uploaded to this %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_items_list'     => sprintf(_x('Filter %s list', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_by_date'        => sprintf(_x('Filter %s by date', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'items_list'            => sprintf(_x('%s list', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published'        => sprintf(_x('%s published.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published_privately' => sprintf(_x('%s published privately.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_reverted_to_draft' => sprintf(_x('%s reverted to draft.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_scheduled'        => sprintf(_x('%s scheduled.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_updated'          => sprintf(_x('%s updated.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_link'             => sprintf(_x('%s link', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'item_link_description' => sprintf(_x('A link to %s', 'post type slug', 'my-custom-post-types'), $post_type),
    ),
    'public' => true,
    'show_in_rest' => true,
    'menu_icon' => $menu_icon,
    'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
    'has_archive' => true,
    'rewrite' => array(
      'slug'       => $post_type,
      'with_front' => false,
      'feeds'      => true,
      'pages'      => true,
    ),
  );

  register_post_type($post_type, $args);
}

/**
* Registers a custom taxonomy.
*
* @param string      $post_type              The post type to associate the taxonomy with.
* @param string      $taxonomy_name          The plural label for the taxonomy.
* @param string      $taxonomy_singular_name The singular label for the taxonomy.
* @param bool        $hierarchical           Whether the taxonomy is hierarchical (default: true).
* @param string|null $taxonomy_slug          The custom taxonomy slug (auto-generated if null).
*/
function my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical = true, $taxonomy_slug = null) {

  if ($taxonomy_slug === null) {
    $taxonomy_slug = create_slug($taxonomy_singular_name);
  }

  $args = array(
    'labels' => array(
      'name'                  => __($taxonomy_name, 'my-custom-post-types'),
      'singular_name'         => __($taxonomy_singular_name, 'my-custom-post-types'),
      'menu_name'             => __($taxonomy_name, 'my-custom-post-types'),
      // translators: %s is the taxonomy name.
      'all_items'             => sprintf(_x('All %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'view_item'             => sprintf(_x('View %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'update_item'           => sprintf(_x('Update %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'new_item_name'         => sprintf(_x('New %s Name', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item'           => sprintf(_x('Parent %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'search_items'          => sprintf(_x('Search %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'not_found'             => sprintf(_x('No %s found.', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'no_terms'              => sprintf(_x('No %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'filter_by_item'        => sprintf(_x('Filter by %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'items_list'            => sprintf(_x('%s list', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'back_to_items'         => sprintf(_x('← Back to %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'item_link'             => sprintf(_x('%s link', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'item_link_description' => sprintf(_x('A link to %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
    ),
    'public' => true,
    'show_ui' => true,
    'hierarchical' => $hierarchical,
    'show_in_rest' => true,
    'rewrite' => array('slug' => $taxonomy_slug),
  );

  register_taxonomy($taxonomy_slug, $post_type, $args);
}

// init フックを使って、カスタム投稿タイプやタクソノミーを登録
// 無名関数の中で $option_name を使えるようにするため use ($option_name) を指定
add_action('init', function () use ($option_name) {
  // wp_options テーブルから設定データを取得(設定がまだ保存されていない場合のデフォルト値として [] を指定)
  $options = get_option($option_name, []);
  // post_type_name(複数形の名前)と post_type_singular_name(単数形の名前)が空なら、カスタム投稿タイプを登録する意味がないので処理を中断
  if (empty($options['post_type_name']) || empty($options['post_type_singular_name'])) {
    return;
  }
  // カスタム投稿タイプの 複数形 の名前
  $name = $options['post_type_name'];
  // カスタム投稿タイプの 単数形 の名前
  $singular_name = $options['post_type_singular_name'];
  // カスタム投稿タイプのスラッグ。指定がなければ create_slug($singular_name) を使って自動生成。
  $post_type_slug = !empty($options['post_type_slug']) ? $options['post_type_slug'] : create_slug($singular_name);
  // 管理画面のメニューアイコン
  $menu_icon = !empty($options['menu_icon']) ? $options['menu_icon'] : 'dashicons-admin-post';

  // カスタム投稿タイプを登録
  my_custom_post_types_register_post_type($name, $singular_name, $post_type_slug, $menu_icon);

  // taxonomy_name(複数形の名前)と taxonomy_singular_name(単数形の名前)が空なら、タクソノミーを登録する意味がないので処理を中断
  if (empty($options['taxonomy_name']) || empty($options['taxonomy_singular_name'])) {
    return;
  }

  // タクソノミーを適用するカスタム投稿タイプ($post_type_slug を使用)
  $post_type = $post_type_slug;
  // タクソノミーの 複数形 の名前
  $taxonomy_name = $options['taxonomy_name'];
  // タクソノミーの 単数形 の名前
  $taxonomy_singular_name = $options['taxonomy_singular_name'];
  // 階層構造を持つかどうか(カテゴリのような構造にするか)
  // isset() で taxonomy_hierarchical が定義されているか確認し、(bool) で true または false に変換。
  $hierarchical = isset($options['taxonomy_hierarchical']) && (bool) $options['taxonomy_hierarchical'];

  // タクソノミーを登録
  my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical);
});

最後の部分のカスタム投稿タイプやタクソノミーの登録(242-278行目)は、以下のように無名関数を使わずに名前付き関数を定義し、add_action() に渡すこともできます。

use ($option_name) は、外部のスコープにある変数をクロージャ(無名関数)内に渡すため、global を使わなくても済みますが、名前付き関数では global $option_name; を使わないと、グローバル変数を参照できません。


function my_custom_post_types_init() {
  // グローバル変数を明示的に参照
  global $option_name;

  $options = get_option($option_name, []);
  if (empty($options['post_type_name']) || empty($options['post_type_singular_name'])) {
    return;
  }

  $name = $options['post_type_name'];
  $singular_name = $options['post_type_singular_name'];
  $post_type_slug = !empty($options['post_type_slug']) ? $options['post_type_slug'] : create_slug($singular_name);
  $menu_icon = !empty($options['menu_icon']) ? $options['menu_icon'] : 'dashicons-admin-post';

  my_custom_post_types_register_post_type($name, $singular_name, $post_type_slug, $menu_icon);

  if (empty($options['taxonomy_name']) || empty($options['taxonomy_singular_name'])) {
    return;
  }

  $post_type = $post_type_slug;
  $taxonomy_name = $options['taxonomy_name'];
  $taxonomy_singular_name = $options['taxonomy_singular_name'];
  $hierarchical = isset($options['taxonomy_hierarchical']) && (bool) $options['taxonomy_hierarchical'];

  my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical);
}

// `init` フックに関数を登録
add_action('init', 'my_custom_post_types_init');
設定を1つの配列として保存

上記の例では、保存する設定を register_setting() で登録する際、すべての設定値を配列として $option_name に保存しているので、個々のフィールドを WordPress に登録する必要がありません。

ページの HTML を出力する際は、settings_fields() を使用してフォームの隠しセキュリティフィールドを出力し、add_settings_section() や add_settings_field() は使用していません。

また、register_setting() を使ってオプションを登録する際に、1つのキー (my_cpt_settings) に対して 連想配列形式 のデータを保存しているので、フォームの name 属性を my_cpt_settings[キー名] の形式にすることで、$_POST に送信されるデータが自動的に配列として処理されます。

設定を1つの配列として保存するメリット

  • シンプルにフォームを作成できる

デメリット

  • 個別の設定項目にバリデーションやサニタイズの処理を追加するのがやや面倒
  • 設定画面の構造をカスタマイズしにくい
個別にフィールドを登録

個別にフィールドを登録したい場合は、add_settings_section() や add_settings_field() を使います。この方法では、次の3つの関数を使って admin_init フックの中で設定フィールドを登録します。

メリット

  • 設定フィールドを動的に追加できる
  • 設定値ごとにカスタムのバリデーション・サニタイズ処理を登録しやすい
  • do_settings_sections() を使うことで、WordPressの標準設定画面に統一感が出る

デメリット

  • add_settings_section() と add_settings_field() の記述が増えるため、やや複雑

以下は前述のコードを add_settings_section() と add_settings_field() を使って、個別にセクションとフィールドを追加するように書き換えたものです。

add_settings_section() でカスタム投稿タイプ用とカスタムタクソノミー用の2つのセクションを追加し、add_settings_field() でフィールドを個別にセクションに追加します。

設定ページで投稿タイプのスラッグの入力を省略した場合は、保存時に自動的に生成した値を入力フィールドに表示するように変更しています。

また、JavaScript を使って、投稿タイプ名(最大20文字)とカスタムタクソノミー名(最大32文字)のスラッグの文字数を検証する処理を追加しています。

<?php

/**
* Plugin Name: My Custom Post Types
* Description: A plugin to create custom post types
* Version: 0.2.1
* Text Domain: my-custom-post-types
*
*/

if (! defined('ABSPATH')) {
  exit;
}

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_cpt_add_admin_menu() {
  add_options_page(
    __('My Custom Post Type Settings', 'my-custom-post-types'),
    __('My CPT Settings', 'my-custom-post-types'),
    'manage_options',
    'my-cpt-settings',
    'my_cpt_settings_page'
  );
}
add_action('admin_menu', 'my_cpt_add_admin_menu');

/**
* 設定ページの出力(設定ページを描画するコールバック関数を定義)
*/
function my_cpt_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My Custom Post Types Settings', 'my-custom-post-types'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_cpt_settings_group');
      do_settings_sections('my-cpt-settings');
      submit_button();
      ?>
    </form>
  </div>
  <script>
    // 投稿タイプ名とカスタムタクソノミー名のスラッグの文字数を検証
    document.addEventListener('DOMContentLoaded', () => {
      const form = document.querySelector('form[action="options.php"]');
      if (!form) return;
      form.addEventListener('submit', (e) => {
        // 検証する入力フィールドを表すオブジェクトの配列
        const fields = [
          { input: document.querySelector('input[name="post_type_slug"]'), limit: 20 },
          { input: document.querySelector('input[name="post_type_singular_name"]'), limit: 20 },
          { input: document.querySelector('input[name="taxonomy_singular_name"]'), limit: 32 },
        ];

        fields.forEach(({ input, limit }) => {
          if (!input) return;

          // 既存のエラーメッセージを削除
          let prevWarning = input.nextElementSibling;
          if (prevWarning && prevWarning.classList.contains('validation-warning')) {
            prevWarning.remove();
          }

          // バリデーション
          if ([...input.value].length > limit) {
            e.preventDefault();
            const warningElem = document.createElement('p');
            warningElem.setAttribute('style', 'color: red');
            warningElem.classList.add('validation-warning');
            warningElem.textContent = `Must not exceed ${limit} characters`;
            input.after(warningElem);
          }
        });
      });
    });
  </script>
<?php
}

/**
* 設定項目の登録
*/
function my_cpt_settings_init() {

  // 設定項目を登録
  register_setting('my_cpt_settings_group', 'post_type_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'post_type_singular_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'post_type_slug', ['sanitize_callback' => 'sanitize_key']);
  register_setting('my_cpt_settings_group', 'menu_icon', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'taxonomy_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'taxonomy_singular_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'taxonomy_hierarchical', ['sanitize_callback' => 'absint']);

  // 設定ページにセクション(Custom Post Type 用)を追加
  add_settings_section(
    'my_cpt_custom_post_type_section',
    __('Custom Post Type Settings', 'my-custom-post-types'),
    function () {
      echo '<p>' . __('Enter the Custom Post Type information', 'my-custom-post-types') . '</p>';
    },
    'my-cpt-settings'
  );

  // セクション内に個々の入力フィールドを追加
  add_settings_field(
    'my_cpt_post_type_name_input',
    __('Post Type Name (Plural)', 'my-custom-post-types'),
    'my_cpt_post_type_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );
  add_settings_field(
    'my_cpt_post_type_singular_name_input',
    __('Post Type Name (Singular)', 'my-custom-post-types'),
    'my_cpt_post_type_singular_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );
  add_settings_field(
    'my_cpt_post_type_slug_input',
    __('Post Type Slug', 'my-custom-post-types'),
    'my_cpt_post_type_slug_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );
  add_settings_field(
    'my_cpt_post_type_menu_icon_input',
    __('Menu Icon', 'my-custom-post-types'),
    'my_cpt_post_type_menu_icon_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );

  // 設定ページにセクション(Custom Taxonomy 用)を追加
  add_settings_section(
    'my_cpt_custom_taxonomy_section',
    __('Custom Taxonomy Settings', 'my-custom-post-types'),
    function () {
      echo '<p>' . __('Enter the Custom Taxonomy information', 'my-custom-post-types') . '</p>';
    },
    'my-cpt-settings'
  );

  // セクション内に個々の入力フィールドを追加
  add_settings_field(
    'my_cpt_taxonomy_name_input',
    __('Taxonomy Name (Plural)', 'my-custom-post-types'),
    'my_cpt_taxonomy_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_taxonomy_section'
  );
  add_settings_field(
    'my_cpt_taxonomy_singular_name_input',
    __('Taxonomy Name (Singular)', 'my-custom-post-types'),
    'my_cpt_taxonomy_singular_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_taxonomy_section'
  );
  add_settings_field(
    'my_cpt_taxonomy_hierarchical_input',
    __('Taxonomy Hierarchical', 'my-custom-post-types'),
    'my_cpt_taxonomy_hierarchical_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_taxonomy_section'
  );
}
add_action('admin_init', 'my_cpt_settings_init');

/**
* 設定フィールドの出力
*/
function my_cpt_post_type_name_input_field_callback() {
  $post_type_name = get_option('post_type_name') ? get_option('post_type_name') : '';
?>
  <input type="text" name="post_type_name" value="<?php echo esc_attr($post_type_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_post_type_singular_name_input_field_callback() {
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
?>
  <input type="text" name="post_type_singular_name" value="<?php echo esc_attr($post_type_singular_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_post_type_slug_input_field_callback() {
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
  $post_type_slug = get_option('post_type_slug') ? get_option('post_type_slug') : sanitize_title($post_type_singular_name);
?>
  <input type="text" name="post_type_slug" value="<?php echo esc_attr($post_type_slug); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('If omitted, it will be generated automatically.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_post_type_menu_icon_input_field_callback() {
  $menu_icon = get_option('menu_icon') ? get_option('menu_icon') : '';
?>
  <input type="text" name="menu_icon" value="<?php echo esc_attr($menu_icon); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Dash Icon Name (Optional)', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_taxonomy_name_input_field_callback() {
  $taxonomy_name = get_option('taxonomy_name') ? get_option('taxonomy_name') : '';
?>
  <input type="text" name="taxonomy_name" value="<?php echo esc_attr($taxonomy_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_taxonomy_singular_name_input_field_callback() {
  $taxonomy_singular_name = get_option('taxonomy_singular_name') ? get_option('taxonomy_singular_name') : '';
?>
  <input type="text" name="taxonomy_singular_name" value="<?php echo esc_attr($taxonomy_singular_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_taxonomy_hierarchical_input_field_callback() {
  $taxonomy_hierarchical = get_option('taxonomy_hierarchical') ? get_option('taxonomy_hierarchical') : '0';
?>
  <input type="checkbox" name="taxonomy_hierarchical" value="1" <?php checked(1, (int) $taxonomy_hierarchical); ?>><?php _e('Enable hierarchy', 'my-custom-post-types'); ?>
  <p class="description"><?php esc_html_e('Check to enable hierarchy', 'my-custom-post-types'); ?></p>
<?php
}

/**
* Registers a custom post type.
*
* @param string      $name          The plural label for the post type.
* @param string      $singular_name The singular label for the post type.
* @param string|null $post_type     The custom post type slug (auto-generated if null).
* @param string      $menu_icon     The Dashicons class for the menu icon.
*/
function my_custom_post_types_register_post_type($name, $singular_name, $post_type, $menu_icon) {

  if ($post_type === null) {
    $post_type = sanitize_title($singular_name);
  }

  if ($menu_icon === null) {
    $menu_icon = 'dashicons-admin-post';
  }

  $args = array(
    'labels'       => array(
      'name'                  => __($name, 'my-custom-post-types'),
      'singular_name'         => __($singular_name, 'my-custom-post-types'),
      'menu_name'             => __($name, 'my-custom-post-types'),
      // translators: %s is the post type name.
      'all_items'             => sprintf(_x('All %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the singular post type name.
      'view_item'             => sprintf(_x('View %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'view_items'            => sprintf(_x('View %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      'add_new'               => __('Add New', 'my-custom-post-types'),
      // translators: %s is the singular post type name.
      'new_item'              => sprintf(_x('New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'search_items'          => sprintf(_x('Search %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'not_found'             => sprintf(_x('No %s found.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'not_found_in_trash'    => sprintf(_x('No %s found in Trash.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'archives'              => sprintf(_x('%s Archives', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'attributes'            => sprintf(_x('%s Attributes', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'insert_into_item'      => sprintf(_x('Insert into %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'uploaded_to_this_item' => sprintf(_x('Uploaded to this %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_items_list'     => sprintf(_x('Filter %s list', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_by_date'        => sprintf(_x('Filter %s by date', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'items_list'            => sprintf(_x('%s list', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published'        => sprintf(_x('%s published.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published_privately' => sprintf(_x('%s published privately.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_reverted_to_draft' => sprintf(_x('%s reverted to draft.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_scheduled'        => sprintf(_x('%s scheduled.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_updated'          => sprintf(_x('%s updated.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_link'             => sprintf(_x('%s link', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'item_link_description' => sprintf(_x('A link to %s', 'post type slug', 'my-custom-post-types'), $post_type),
    ),
    'public' => true,
    'show_in_rest' => true,
    'menu_icon' => $menu_icon,
    'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
    'has_archive' => true,
    'rewrite' => array(
      'slug'       => $post_type,
      'with_front' => false,
      'feeds'      => true,
      'pages'      => true,
    ),
  );

  register_post_type($post_type, $args);
}

/**
* Registers a custom taxonomy.
*
* @param string      $post_type              The post type to associate the taxonomy with.
* @param string      $taxonomy_name          The plural label for the taxonomy.
* @param string      $taxonomy_singular_name The singular label for the taxonomy.
* @param bool        $hierarchical           Whether the taxonomy is hierarchical (default: true).
* @param string|null $taxonomy_slug          The custom taxonomy slug (auto-generated if null).
*/
function my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical = true, $taxonomy_slug = null) {

  if ($taxonomy_slug === null) {
    $taxonomy_slug = sanitize_title($taxonomy_singular_name);
  }

  $args = array(
    'labels' => array(
      'name'                  => __($taxonomy_name, 'my-custom-post-types'),
      'singular_name'         => __($taxonomy_singular_name, 'my-custom-post-types'),
      'menu_name'             => __($taxonomy_name, 'my-custom-post-types'),
      // translators: %s is the taxonomy name.
      'all_items'             => sprintf(_x('All %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'view_item'             => sprintf(_x('View %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'update_item'           => sprintf(_x('Update %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'new_item_name'         => sprintf(_x('New %s Name', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item'           => sprintf(_x('Parent %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'search_items'          => sprintf(_x('Search %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'not_found'             => sprintf(_x('No %s found.', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'no_terms'              => sprintf(_x('No %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'filter_by_item'        => sprintf(_x('Filter by %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'items_list'            => sprintf(_x('%s list', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'back_to_items'         => sprintf(_x('← Back to %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'item_link'             => sprintf(_x('%s link', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'item_link_description' => sprintf(_x('A link to %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
    ),
    'public' => true,
    'show_ui' => true,
    'hierarchical' => $hierarchical,
    'show_in_rest' => true,
    'rewrite' => array('slug' => $taxonomy_slug),
  );

  register_taxonomy($taxonomy_slug, $post_type, $args);
}

function my_custom_post_types_init() {

  $post_type_name = get_option('post_type_name') ? get_option('post_type_name') : '';
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
  $post_type_slug = get_option('post_type_slug') ? get_option('post_type_slug') : sanitize_title($post_type_singular_name);
  $menu_icon = get_option('menu_icon') ? get_option('menu_icon') : 'dashicons-admin-post';

  if (empty($post_type_name) || empty($post_type_singular_name)) {
    return;
  }
  my_custom_post_types_register_post_type($post_type_name, $post_type_singular_name, $post_type_slug, $menu_icon);

  $taxonomy_name = get_option('taxonomy_name') ? get_option('taxonomy_name') : '';
  $taxonomy_singular_name = get_option('taxonomy_singular_name') ? get_option('taxonomy_singular_name') : '';
  $hierarchical =  (bool) get_option('taxonomy_hierarchical');

  if (empty($taxonomy_name) || empty($taxonomy_singular_name)) {
    return;
  }

  my_custom_post_types_register_taxonomy($post_type_slug, $taxonomy_name, $taxonomy_singular_name, $hierarchical);
}

add_action('init', 'my_custom_post_types_init');

例えば、以下のような設定ページが表示されます。

JavaScript を翻訳可能に

この例では従来の wp_localize_script 関数を用いた翻訳の方法を使います。

前述のコードでは、スラッグの文字数を検証する JavaScript の処理を、設定ページを描画するコールバック関数 my_cpt_settings_page() の中で script タグを使って記述していましたが、別途 JavaScript ファイルを作成して読み込むように変更します。

コールバック関数 my_cpt_settings_page() から JavaScript 部分をコピーして script タグを削除します。

/**
 * 設定ページの出力(JavaScript を削除)
 */
function my_cpt_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My Custom Post Types Settings', 'my-custom-post-types'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_cpt_settings_group');
      do_settings_sections('my-cpt-settings');
      submit_button();
      ?>
    </form>
  </div>
<?php
}

プラグインのフォルダ内に assets フォルダを作成し、その中に my-custom-post-types-admin.js という名前の JavaScript ファイルを作成して、コピーしたコードをペーストします(ファイル名は任意)。

my-custom-post-types/
  ├── assets/
  │   └── my-custom-post-types-admin.js
  └── my-custom-post-types.php

プラグインファイル(my-custom-post-types.php)に以下の記述を追加します。

以下の 9-19 行目では、作成した JavaScript ファイル(my-custom-post-types-admin.js)をプラグインの設定ページでのみエンキューします(管理画面にエンキュー)。

そして、wp_localize_script() を使って、読み込んだ JavaScript ファイルに翻訳関数を適用した文字列(PHP のデータ)を JavaScript のオブジェクト(myCPTAdmin.validationMessage)として渡します。

23行目は、翻訳文字列のプレースホルダー %d の意味を説明するための translators: コメントです。

add_action('admin_enqueue_scripts', 'my_custom_post_types_admin_scripts');

function my_custom_post_types_admin_scripts($hook) {

  // プラグインの設定ページのスラッグ
  $allowed_pages = ['settings_page_my-cpt-settings'];

  // $hook が対象のページであれば JavaScript をエンキュー
  if (in_array($hook, $allowed_pages)) {
    $plugin_dir = plugin_dir_path(__FILE__);
    $plugin_url = plugin_dir_url(__FILE__);
    $script_file   = 'assets/my-custom-post-types-admin.js';
    wp_enqueue_script(
      'my-custom-post-types-admin-script',
      $plugin_url . $script_file,
      array(),
      filemtime($plugin_dir . $script_file),
      true
    );

    // 翻訳文字列を渡す
    wp_localize_script('my-custom-post-types-admin-script', 'myCPTAdmin', array(
      // translators: %d is the max number for the slug name validation.
      'validationMessage' => __('Must not exceed %d characters', 'my-custom-post-types'),
    ));
  }
}
wp_localize_script()

wp_localize_script() は、翻訳(ローカライズ)用として作成された PHP から JavaScript に データを渡すことができる関数です。

wp_localize_script($handle, $object_name, $data);
パラメータ 説明
$handle(必須) wp_enqueue_script で登録したスクリプトのハンドル名(識別子)
$object_name(必須) JavaScript 側でグローバル変数として使用するオブジェクト名
$data(必須) JavaScript に渡すデータ(連想配列)

wp_localize_script は、本来は翻訳(ローカライズ)用の関数として作られましたが、JavaScript へ任意のデータを渡す方法としても使われます。但し、現在は JavaScript へデータを渡す方法としては wp_add_inline_script() を使用することが推奨されています 。

この例では以下のように、翻訳関数を適用した文字列 __('Must not exceed %d characters', 'my-custom-post-types') を myCPTAdmin というオブジェクトの validationMessage プロパティとして、ハンドル名に指定した JavaScript に渡しています。

// 翻訳文字列を渡す
wp_localize_script(
  'my-custom-post-types-admin-script', // スクリプトのハンドル名
  'myCPTAdmin',  // オブジェクト名
  array(
    // JavaScript に渡すデータ
    'validationMessage' => __('Must not exceed %d characters', 'my-custom-post-types'),
  )
);

上記 wp_localize_script() により、第1引数に指定した JavaScript ファイルを読み込んでいる設定ページのソースコードには以下のような script タグが出力されます。

これにより、JavaScript ファイルでは 変数 myCPTAdmin を使って、myCPTAdmin.validationMessage で(オブジェクト myCPTAdmin の validationMessage プロパティとして)翻訳が適用された文字列にアクセスすることができます。

<script id="my-custom-post-types-admin-script-js-extra">
  var myCPTAdmin = {"validationMessage":"Must not exceed %d characters"};
</script>

JavaScript を修正

JavaScript の翻訳対象部分を wp_localize_script() で渡された myCPTAdmin.validationMessage を参照するように変更します。

myCPTAdmin.validationMessage には PHP の翻訳関数が適用された __('Must not exceed %d characters', 'my-custom-post-types') の値が入っています。

その際、replace('%d', limit) で、プレースホルダー文字 %d を変数 limit に置換します (26行目)。

document.addEventListener('DOMContentLoaded', () => {
  const form = document.querySelector('form[action="options.php"]');
  if (!form) return;

  form.addEventListener('submit', (e) => {
    const fields = [
      { input: document.querySelector('input[name="post_type_slug"]'), limit: 20 },
      { input: document.querySelector('input[name="post_type_singular_name"]'), limit: 20 },
      { input: document.querySelector('input[name="taxonomy_singular_name"]'), limit: 32 },
    ];

    fields.forEach(({ input, limit }) => {
      if (!input) return;

      let prevWarning = input.nextElementSibling;
      if (prevWarning && prevWarning.classList.contains('validation-warning')) {
        prevWarning.remove();
      }

      if ([...input.value].length > limit) {
        e.preventDefault();
        const warningElem = document.createElement('p');
        warningElem.setAttribute('style', 'color: red');
        warningElem.classList.add('validation-warning');
        // warningElem.textContent = `Must not exceed ${limit} characters`; は以下に書き換え
        warningElem.textContent = myCPTAdmin.validationMessage.replace('%d', limit);
        input.after(warningElem);
      }
    });
  });
});

プラグインファイルのコード全体は以下のようになります。

<?php

/**
* Plugin Name: My Custom Post Types
* Description: A plugin to create custom post types
* Version: 0.2.2
* Text Domain: my-custom-post-types
*
*/

if (! defined('ABSPATH')) {
  exit;
}

/**
* 管理画面のメニューに独自の設定ページを追加
*/
function my_cpt_add_admin_menu() {
  add_options_page(
    __('My Custom Post Type Settings', 'my-custom-post-types'),
    __('My CPT Settings', 'my-custom-post-types'),
    'manage_options',
    'my-cpt-settings',
    'my_cpt_settings_page'
  );
}
add_action('admin_menu', 'my_cpt_add_admin_menu');

/**
* 設定ページの出力(設定ページを描画するコールバック関数を定義)
*/
function my_cpt_settings_page() {
?>
  <div class="wrap">
    <h1><?php esc_html_e('My Custom Post Types Settings', 'my-custom-post-types'); ?></h1>
    <form method="post" action="options.php">
      <?php
      settings_fields('my_cpt_settings_group');
      do_settings_sections('my-cpt-settings');
      submit_button();
      ?>
    </form>
  </div>
<?php
}

/**
* 設定項目の登録
*/
function my_cpt_settings_init() {

  // 設定項目を登録
  register_setting('my_cpt_settings_group', 'post_type_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'post_type_singular_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'post_type_slug', ['sanitize_callback' => 'sanitize_key']);
  register_setting('my_cpt_settings_group', 'menu_icon', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'taxonomy_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'taxonomy_singular_name', ['sanitize_callback' => 'sanitize_text_field']);
  register_setting('my_cpt_settings_group', 'taxonomy_hierarchical', ['sanitize_callback' => 'absint']);

  // 設定ページにセクション(Custom Post Type 用)を追加
  add_settings_section(
    'my_cpt_custom_post_type_section',
    __('Custom Post Type Settings', 'my-custom-post-types'),
    function () {
      echo '<p>' . __('Enter the Custom Post Type information', 'my-custom-post-types') . '</p>';
    },
    'my-cpt-settings'
  );

  // セクション内に個々の入力フィールドを追加
  add_settings_field(
    'my_cpt_post_type_name_input',
    __('Post Type Name (Plural)', 'my-custom-post-types'),
    'my_cpt_post_type_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );
  add_settings_field(
    'my_cpt_post_type_singular_name_input',
    __('Post Type Name (Singular)', 'my-custom-post-types'),
    'my_cpt_post_type_singular_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );
  add_settings_field(
    'my_cpt_post_type_slug_input',
    __('Post Type Slug', 'my-custom-post-types'),
    'my_cpt_post_type_slug_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );
  add_settings_field(
    'my_cpt_post_type_menu_icon_input',
    __('Menu Icon', 'my-custom-post-types'),
    'my_cpt_post_type_menu_icon_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_post_type_section'
  );

  // 設定ページにセクション(Custom Taxonomy 用)を追加
  add_settings_section(
    'my_cpt_custom_taxonomy_section',
    __('Custom Taxonomy Settings', 'my-custom-post-types'),
    function () {
      echo '<p>' . __('Enter the Custom Taxonomy information', 'my-custom-post-types') . '</p>';
    },
    'my-cpt-settings'
  );

  // セクション内に個々の入力フィールドを追加
  add_settings_field(
    'my_cpt_taxonomy_name_input',
    __('Taxonomy Name (Plural)', 'my-custom-post-types'),
    'my_cpt_taxonomy_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_taxonomy_section'
  );
  add_settings_field(
    'my_cpt_taxonomy_singular_name_input',
    __('Taxonomy Name (Singular)', 'my-custom-post-types'),
    'my_cpt_taxonomy_singular_name_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_taxonomy_section'
  );
  add_settings_field(
    'my_cpt_taxonomy_hierarchical_input',
    __('Taxonomy Hierarchical', 'my-custom-post-types'),
    'my_cpt_taxonomy_hierarchical_input_field_callback',
    'my-cpt-settings',
    'my_cpt_custom_taxonomy_section'
  );
}
add_action('admin_init', 'my_cpt_settings_init');

/**
* 設定フィールドの出力
*/
function my_cpt_post_type_name_input_field_callback() {
  $post_type_name = get_option('post_type_name') ? get_option('post_type_name') : '';
?>
  <input type="text" name="post_type_name" value="<?php echo esc_attr($post_type_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_post_type_singular_name_input_field_callback() {
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
?>
  <input type="text" name="post_type_singular_name" value="<?php echo esc_attr($post_type_singular_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_post_type_slug_input_field_callback() {
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
  $post_type_slug = get_option('post_type_slug') ? get_option('post_type_slug') : sanitize_title($post_type_singular_name);
?>
  <input type="text" name="post_type_slug" value="<?php echo esc_attr($post_type_slug); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('If omitted, it will be generated automatically.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_post_type_menu_icon_input_field_callback() {
  $menu_icon = get_option('menu_icon') ? get_option('menu_icon') : '';
?>
  <input type="text" name="menu_icon" value="<?php echo esc_attr($menu_icon); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Dash Icon Name (Optional)', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_taxonomy_name_input_field_callback() {
  $taxonomy_name = get_option('taxonomy_name') ? get_option('taxonomy_name') : '';
?>
  <input type="text" name="taxonomy_name" value="<?php echo esc_attr($taxonomy_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_taxonomy_singular_name_input_field_callback() {
  $taxonomy_singular_name = get_option('taxonomy_singular_name') ? get_option('taxonomy_singular_name') : '';
?>
  <input type="text" name="taxonomy_singular_name" value="<?php echo esc_attr($taxonomy_singular_name); ?>" class="regular-text">
  <p class="description"><?php esc_html_e('Required.', 'my-custom-post-types'); ?></p>
<?php
}

function my_cpt_taxonomy_hierarchical_input_field_callback() {
  $taxonomy_hierarchical = get_option('taxonomy_hierarchical') ? get_option('taxonomy_hierarchical') : '0';
?>
  <input type="checkbox" name="taxonomy_hierarchical" value="1" <?php checked(1, (int) $taxonomy_hierarchical); ?>><?php _e('Enable hierarchy', 'my-custom-post-types'); ?>
  <p class="description"><?php esc_html_e('Check to enable hierarchy', 'my-custom-post-types'); ?></p>
<?php
}

/**
* Registers a custom post type.
*
* @param string      $name          The plural label for the post type.
* @param string      $singular_name The singular label for the post type.
* @param string|null $post_type     The custom post type slug (auto-generated if null).
* @param string      $menu_icon     The Dashicons class for the menu icon.
*/
function my_custom_post_types_register_post_type($name, $singular_name, $post_type, $menu_icon) {

  if ($post_type === null) {
    $post_type = sanitize_title($singular_name);
  }

  if ($menu_icon === null) {
    $menu_icon = 'dashicons-admin-post';
  }

  $args = array(
    'labels'       => array(
      'name'                  => __($name, 'my-custom-post-types'),
      'singular_name'         => __($singular_name, 'my-custom-post-types'),
      'menu_name'             => __($name, 'my-custom-post-types'),
      // translators: %s is the post type name.
      'all_items'             => sprintf(_x('All %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the singular post type name.
      'view_item'             => sprintf(_x('View %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'view_items'            => sprintf(_x('View %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the singular post type name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      'add_new'               => __('Add New', 'my-custom-post-types'),
      // translators: %s is the singular post type name.
      'new_item'              => sprintf(_x('New %s', 'singular post type', 'my-custom-post-types'), $singular_name),
      // translators: %s is the post type name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'search_items'          => sprintf(_x('Search %s', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'not_found'             => sprintf(_x('No %s found.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'not_found_in_trash'    => sprintf(_x('No %s found in Trash.', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'archives'              => sprintf(_x('%s Archives', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'attributes'            => sprintf(_x('%s Attributes', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'insert_into_item'      => sprintf(_x('Insert into %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'uploaded_to_this_item' => sprintf(_x('Uploaded to this %s', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_items_list'     => sprintf(_x('Filter %s list', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type slug.
      'filter_by_date'        => sprintf(_x('Filter %s by date', 'post type slug', 'my-custom-post-types'), $post_type),
      // translators: %s is the post type name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'items_list'            => sprintf(_x('%s list', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published'        => sprintf(_x('%s published.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_published_privately' => sprintf(_x('%s published privately.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_reverted_to_draft' => sprintf(_x('%s reverted to draft.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_scheduled'        => sprintf(_x('%s scheduled.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_updated'          => sprintf(_x('%s updated.', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type name.
      'item_link'             => sprintf(_x('%s link', 'post type', 'my-custom-post-types'), $name),
      // translators: %s is the post type slug.
      'item_link_description' => sprintf(_x('A link to %s', 'post type slug', 'my-custom-post-types'), $post_type),
    ),
    'public' => true,
    'show_in_rest' => true,
    'menu_icon' => $menu_icon,
    'supports' => array('title', 'editor', 'thumbnail', 'custom-fields'),
    'has_archive' => true,
    'rewrite' => array(
      'slug'       => $post_type,
      'with_front' => false,
      'feeds'      => true,
      'pages'      => true,
    ),
  );

  register_post_type($post_type, $args);
}

/**
* Registers a custom taxonomy.
*
* @param string      $post_type              The post type to associate the taxonomy with.
* @param string      $taxonomy_name          The plural label for the taxonomy.
* @param string      $taxonomy_singular_name The singular label for the taxonomy.
* @param bool        $hierarchical           Whether the taxonomy is hierarchical (default: true).
* @param string|null $taxonomy_slug          The custom taxonomy slug (auto-generated if null).
*/
function my_custom_post_types_register_taxonomy($post_type, $taxonomy_name, $taxonomy_singular_name, $hierarchical = true, $taxonomy_slug = null) {

  if ($taxonomy_slug === null) {
    $taxonomy_slug = sanitize_title($taxonomy_singular_name);
  }

  $args = array(
    'labels' => array(
      'name'                  => __($taxonomy_name, 'my-custom-post-types'),
      'singular_name'         => __($taxonomy_singular_name, 'my-custom-post-types'),
      'menu_name'             => __($taxonomy_name, 'my-custom-post-types'),
      // translators: %s is the taxonomy name.
      'all_items'             => sprintf(_x('All %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'edit_item'             => sprintf(_x('Edit %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'view_item'             => sprintf(_x('View %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'update_item'           => sprintf(_x('Update %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'add_new_item'          => sprintf(_x('Add New %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'new_item_name'         => sprintf(_x('New %s Name', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item'           => sprintf(_x('Parent %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'parent_item_colon'     => sprintf(_x('Parent %s:', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'search_items'          => sprintf(_x('Search %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'not_found'             => sprintf(_x('No %s found.', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'no_terms'              => sprintf(_x('No %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'filter_by_item'        => sprintf(_x('Filter by %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the taxonomy name.
      'items_list_navigation' => sprintf(_x('%s list navigation', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'items_list'            => sprintf(_x('%s list', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the taxonomy name.
      'back_to_items'         => sprintf(_x('← Back to %s', 'taxonomy', 'my-custom-post-types'), $taxonomy_name),
      // translators: %s is the singular taxonomy name.
      'item_link'             => sprintf(_x('%s link', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
      // translators: %s is the singular taxonomy name.
      'item_link_description' => sprintf(_x('A link to %s', 'singular taxonomy', 'my-custom-post-types'), $taxonomy_singular_name),
    ),
    'public' => true,
    'show_ui' => true,
    'hierarchical' => $hierarchical,
    'show_in_rest' => true,
    'rewrite' => array('slug' => $taxonomy_slug),
  );

  register_taxonomy($taxonomy_slug, $post_type, $args);
}

function my_custom_post_types_init() {

  $post_type_name = get_option('post_type_name') ? get_option('post_type_name') : '';
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
  $post_type_slug = get_option('post_type_slug') ? get_option('post_type_slug') : sanitize_title($post_type_singular_name);
  $menu_icon = get_option('menu_icon') ? get_option('menu_icon') : 'dashicons-admin-post';

  if (empty($post_type_name) || empty($post_type_singular_name)) {
    return;
  }
  my_custom_post_types_register_post_type($post_type_name, $post_type_singular_name, $post_type_slug, $menu_icon);

  $taxonomy_name = get_option('taxonomy_name') ? get_option('taxonomy_name') : '';
  $taxonomy_singular_name = get_option('taxonomy_singular_name') ? get_option('taxonomy_singular_name') : '';
  $hierarchical =  (bool) get_option('taxonomy_hierarchical');

  if (empty($taxonomy_name) || empty($taxonomy_singular_name)) {
    return;
  }

  my_custom_post_types_register_taxonomy($post_type_slug, $taxonomy_name, $taxonomy_singular_name, $hierarchical);
}
add_action('init', 'my_custom_post_types_init');

// admin_enqueue_scripts アクションフックを使用
add_action('admin_enqueue_scripts', 'my_custom_post_types_admin_scripts');

function my_custom_post_types_admin_scripts($hook) {

  // プラグインの設定ページのスラッグ
  $allowed_pages = ['settings_page_my-cpt-settings'];

  // $hook が対象のページであれば JavaScript をエンキュー
  if (in_array($hook, $allowed_pages)) {
    $plugin_dir = plugin_dir_path(__FILE__);
    $plugin_url = plugin_dir_url(__FILE__);
    $script_file   = 'assets/my-custom-post-types-admin.js';
    wp_enqueue_script(
      'my-custom-post-types-admin-script',
      $plugin_url . $script_file,
      array(),
      filemtime($plugin_dir . $script_file),
      true
    );

    // 翻訳文字列を渡す
    wp_localize_script('my-custom-post-types-admin-script', 'myCPTAdmin', array(
      // translators: %d is the max number for the slug name validation.
      'validationMessage' => __('Must not exceed %d characters', 'my-custom-post-types'),
    ));
  }
}

翻訳ファイルの作成

プラグインの翻訳ファイルを作成する例です。

以下はすでに WordPress の開発者向けコマンドラインツール WP-CLI がインストールされていることを前提にしています。この時点での WP-CLI のバージョンは 2.11.0 です。

おおまかな手順は以下のとおりです。主にコマンドラインからの操作になります。

  1. languages ディレクトリを作成
  2. POT ファイルを作成
  3. PO ファイルを作成
  4. PO ファイルに翻訳を作成
  5. MO ファイルを作成
  6. .l10n.php(PHP)ファイルを作成
  7. MO ファイルをロード

翻訳ファイルの作成の詳細については、よろしければ以下を御覧ください。

languages ディレクトリを作成

プラグインディレクトリの直下に languages ディレクトリを作成します。

例えば、プラグインのディレクトリ(この例の場合は my-custom-post-types)で以下を実行します。

% mkdir languages

すべての言語ファイルはプラグインの languages ディレクトリに配置するのが一般的です。

my-custom-post-types/
  ├── assets/
  │   └── my-custom-post-types-admin.js
  ├── languages // 言語ファイルを格納するディレクトリを追加
  └── my-custom-post-types.php

POT ファイルを作成

以下を実行して languages ディレクトリに my-custom-post-types.pot という名前の POT ファイルを作成します。ファイル名は任意ですが、textdomain の値を使うのが一般的です。

% wp i18n make-pot ./ languages/my-custom-post-types.pot

上記により、翻訳関数が適用された文字列が抽出され、以下のような POT ファイルが生成されます。

# Copyright (C) 2025
# This file is distributed under the same license as the My Custom Post Types plugin.
msgid ""
msgstr ""
"Project-Id-Version: My Custom Post Types 0.2.2\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/my-custom-post-types\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2025-03-09T04:41:44+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.11.0\n"
"X-Domain: my-custom-post-types\n"

#. Plugin Name of the plugin
#: my-custom-post-types.php
msgid "My Custom Post Types"
msgstr ""

#. Description of the plugin
#: my-custom-post-types.php
msgid "A plugin to create custom post types"
msgstr ""

#: my-custom-post-types.php:20
msgid "My Custom Post Type Settings"
msgstr ""

#: my-custom-post-types.php:21
msgid "My CPT Settings"
msgstr ""

#: my-custom-post-types.php:35
msgid "My Custom Post Types Settings"
msgstr ""

#: my-custom-post-types.php:64
msgid "Custom Post Type Settings"
msgstr ""

#: my-custom-post-types.php:66
msgid "Enter the Custom Post Type information"
msgstr ""

#: my-custom-post-types.php:74
msgid "Post Type Name (Plural)"
msgstr ""

#: my-custom-post-types.php:81
msgid "Post Type Name (Singular)"
msgstr ""

#: my-custom-post-types.php:88
msgid "Post Type Slug"
msgstr ""

#: my-custom-post-types.php:95
msgid "Menu Icon"
msgstr ""

#: my-custom-post-types.php:104
msgid "Custom Taxonomy Settings"
msgstr ""

#: my-custom-post-types.php:106
msgid "Enter the Custom Taxonomy information"
msgstr ""

#: my-custom-post-types.php:114
msgid "Taxonomy Name (Plural)"
msgstr ""

#: my-custom-post-types.php:121
msgid "Taxonomy Name (Singular)"
msgstr ""

#: my-custom-post-types.php:128
msgid "Taxonomy Hierarchical"
msgstr ""

#: my-custom-post-types.php:143
#: my-custom-post-types.php:151
#: my-custom-post-types.php:176
#: my-custom-post-types.php:184
msgid "Required."
msgstr ""

#: my-custom-post-types.php:160
msgid "If omitted, it will be generated automatically."
msgstr ""

#: my-custom-post-types.php:168
msgid "Dash Icon Name (Optional)"
msgstr ""

#: my-custom-post-types.php:191
msgid "Enable hierarchy"
msgstr ""

#: my-custom-post-types.php:192
msgid "Check to enable hierarchy"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:220
msgctxt "post type"
msgid "All %s"
msgstr ""

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:222
msgctxt "singular post type"
msgid "Edit %s"
msgstr ""

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:224
msgctxt "singular post type"
msgid "View %s"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:226
msgctxt "post type"
msgid "View %s"
msgstr ""

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:228
msgctxt "singular post type"
msgid "Add New %s"
msgstr ""

#: my-custom-post-types.php:229
msgid "Add New"
msgstr ""

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:231
msgctxt "singular post type"
msgid "New %s"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:233
msgctxt "post type"
msgid "Parent %s:"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:235
msgctxt "post type"
msgid "Search %s"
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:237
msgctxt "post type slug"
msgid "No %s found."
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:239
msgctxt "post type slug"
msgid "No %s found in Trash."
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:241
msgctxt "post type"
msgid "%s Archives"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:243
msgctxt "post type"
msgid "%s Attributes"
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:245
msgctxt "post type slug"
msgid "Insert into %s"
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:247
msgctxt "post type slug"
msgid "Uploaded to this %s"
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:249
msgctxt "post type slug"
msgid "Filter %s list"
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:251
msgctxt "post type slug"
msgid "Filter %s by date"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:253
msgctxt "post type"
msgid "%s list navigation"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:255
msgctxt "post type"
msgid "%s list"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:257
msgctxt "post type"
msgid "%s published."
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:259
msgctxt "post type"
msgid "%s published privately."
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:261
msgctxt "post type"
msgid "%s reverted to draft."
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:263
msgctxt "post type"
msgid "%s scheduled."
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:265
msgctxt "post type"
msgid "%s updated."
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:267
msgctxt "post type"
msgid "%s link"
msgstr ""

#. translators: %s is the post type slug.
#: my-custom-post-types.php:269
msgctxt "post type slug"
msgid "A link to %s"
msgstr ""

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:308
msgctxt "taxonomy"
msgid "All %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:310
msgctxt "singular taxonomy"
msgid "Edit %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:312
msgctxt "singular taxonomy"
msgid "View %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:314
msgctxt "singular taxonomy"
msgid "Update %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:316
msgctxt "singular taxonomy"
msgid "Add New %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:318
msgctxt "singular taxonomy"
msgid "New %s Name"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:320
msgctxt "singular taxonomy"
msgid "Parent %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:322
msgctxt "singular taxonomy"
msgid "Parent %s:"
msgstr ""

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:324
msgctxt "taxonomy"
msgid "Search %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:326
msgctxt "singular taxonomy"
msgid "No %s found."
msgstr ""

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:328
msgctxt "taxonomy"
msgid "No %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:330
msgctxt "singular taxonomy"
msgid "Filter by %s"
msgstr ""

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:332
msgctxt "taxonomy"
msgid "%s list navigation"
msgstr ""

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:334
msgctxt "taxonomy"
msgid "%s list"
msgstr ""

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:336
msgctxt "taxonomy"
msgid "← Back to %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:338
msgctxt "singular taxonomy"
msgid "%s link"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:340
msgctxt "singular taxonomy"
msgid "A link to %s"
msgstr ""

#. translators: %d is the max number for the slug name validation.
#: my-custom-post-types.php:400
msgid "Must not exceed %d characters"
msgstr ""

PO ファイルを作成

POT ファイル(my-custom-post-types.pot)をコピーして PO ファイル(my-custom-post-types-ja.po)を作成します。

コピーする際に、翻訳する言語の言語コードをファイル名の最後に追加し、拡張子を「.po」にします。

この例の場合、日本語(ja)の翻訳を作成するので、「-ja」をファイル名に追加します。

% cp languages/my-custom-post-types.pot languages/my-custom-post-types-ja.po

PO ファイルに翻訳を作成

上記でコピーして作成した PO ファイル(my-custom-post-types-ja.po)をエディターで開きます。

翻訳する言語(この場合は日本語 ja)を設定する、"Language: ja\n" の行を追加し(9行目)、翻訳する msgstr に翻訳の文字列を追加します(空にすると、原文のまま表示されます)。

プレースホルダー文字 %s は翻訳でもそのまま使えます。

国際化対応として追加した translators: コメントや _x( ) 関数で指定したコンテキスト(msgctxt)が、翻訳対象文字列の上に表示されるようになっています。

# Copyright (C) 2025
# This file is distributed under the same license as the My Custom Post Types plugin.
msgid ""
msgstr ""
"Project-Id-Version: My Custom Post Types 0.2.2\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/my-custom-post-types\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2025-03-09T04:41:44+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.11.0\n"
"X-Domain: my-custom-post-types\n"

#. Plugin Name of the plugin
#: my-custom-post-types.php
msgid "My Custom Post Types"
msgstr ""

#. Description of the plugin
#: my-custom-post-types.php
msgid "A plugin to create custom post types"
msgstr "カスタム投稿タイプを作成するプラグイン"

#: my-custom-post-types.php:20
msgid "My Custom Post Type Settings"
msgstr "My Custom Post Type の設定"

#: my-custom-post-types.php:21
msgid "My CPT Settings"
msgstr ""

#: my-custom-post-types.php:35
msgid "My Custom Post Types Settings"
msgstr "My Custom Post Types の設定"

#: my-custom-post-types.php:64
msgid "Custom Post Type Settings"
msgstr "カスタム投稿タイプ設定"

#: my-custom-post-types.php:66
msgid "Enter the Custom Post Type information"
msgstr "カスタム投稿タイプの情報を入力"

#: my-custom-post-types.php:74
msgid "Post Type Name (Plural)"
msgstr "投稿タイプ名(複数形)"

#: my-custom-post-types.php:81
msgid "Post Type Name (Singular)"
msgstr "投稿タイプ名(単数形)"

#: my-custom-post-types.php:88
msgid "Post Type Slug"
msgstr "投稿タイプスラッグ"

#: my-custom-post-types.php:95
msgid "Menu Icon"
msgstr "メニューアイコン"

#: my-custom-post-types.php:104
msgid "Custom Taxonomy Settings"
msgstr "カスタムタクソノミー設定"

#: my-custom-post-types.php:106
msgid "Enter the Custom Taxonomy information"
msgstr "カスタムタクソノミーの情報を入力"

#: my-custom-post-types.php:114
msgid "Taxonomy Name (Plural)"
msgstr "タクソノミー名(複数形"

#: my-custom-post-types.php:121
msgid "Taxonomy Name (Singular)"
msgstr "タクソノミー名(単数形)"

#: my-custom-post-types.php:128
msgid "Taxonomy Hierarchical"
msgstr "階層構造"

#: my-custom-post-types.php:143
#: my-custom-post-types.php:151
#: my-custom-post-types.php:176
#: my-custom-post-types.php:184
msgid "Required."
msgstr "必須"

#: my-custom-post-types.php:160
msgid "If omitted, it will be generated automatically."
msgstr "省略時は自動的に生成されます"

#: my-custom-post-types.php:168
msgid "Dash Icon Name (Optional)"
msgstr "ダッシュアイコンの名前(オプション)"

#: my-custom-post-types.php:191
msgid "Enable hierarchy"
msgstr "階層構造を有効"

#: my-custom-post-types.php:192
msgid "Check to enable hierarchy"
msgstr "階層構造を有効化するにはチェックを入れる"

#. translators: %s is the post type name.
#: my-custom-post-types.php:220
msgctxt "post type"
msgid "All %s"
msgstr "全ての %s"

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:222
msgctxt "singular post type"
msgid "Edit %s"
msgstr "%s を編集"

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:224
msgctxt "singular post type"
msgid "View %s"
msgstr "%s を表示"

#. translators: %s is the post type name.
#: my-custom-post-types.php:226
msgctxt "post type"
msgid "View %s"
msgstr "%s を表示"

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:228
msgctxt "singular post type"
msgid "Add New %s"
msgstr "新規 %s を追加"

#: my-custom-post-types.php:229
msgid "Add New"
msgstr "新規 %s を追加"

#. translators: %s is the singular post type name.
#: my-custom-post-types.php:231
msgctxt "singular post type"
msgid "New %s"
msgstr "新規 %s"

#. translators: %s is the post type name.
#: my-custom-post-types.php:233
msgctxt "post type"
msgid "Parent %s:"
msgstr "親の %s:"

#. translators: %s is the post type name.
#: my-custom-post-types.php:235
msgctxt "post type"
msgid "Search %s"
msgstr "%s を検索"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:237
msgctxt "post type slug"
msgid "No %s found."
msgstr "%s が見つかりませんでした。"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:239
msgctxt "post type slug"
msgid "No %s found in Trash."
msgstr "ゴミ箱に %s が見つかりませんでした。"

#. translators: %s is the post type name.
#: my-custom-post-types.php:241
msgctxt "post type"
msgid "%s Archives"
msgstr "%s アーカイブ"

#. translators: %s is the post type name.
#: my-custom-post-types.php:243
msgctxt "post type"
msgid "%s Attributes"
msgstr "%s 属性"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:245
msgctxt "post type slug"
msgid "Insert into %s"
msgstr "%s に挿入"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:247
msgctxt "post type slug"
msgid "Uploaded to this %s"
msgstr "この %s にアップロードされました"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:249
msgctxt "post type slug"
msgid "Filter %s list"
msgstr "%s リストを絞り込み"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:251
msgctxt "post type slug"
msgid "Filter %s by date"
msgstr "%s を日時で絞り込み"

#. translators: %s is the post type name.
#: my-custom-post-types.php:253
msgctxt "post type"
msgid "%s list navigation"
msgstr ""

#. translators: %s is the post type name.
#: my-custom-post-types.php:255
msgctxt "post type"
msgid "%s list"
msgstr "%s リスト"

#. translators: %s is the post type name.
#: my-custom-post-types.php:257
msgctxt "post type"
msgid "%s published."
msgstr "%s を公開しました。"

#. translators: %s is the post type name.
#: my-custom-post-types.php:259
msgctxt "post type"
msgid "%s published privately."
msgstr "%s をプライベートで公開しました。"

#. translators: %s is the post type name.
#: my-custom-post-types.php:261
msgctxt "post type"
msgid "%s reverted to draft."
msgstr "%s を下書きに戻しました。"

#. translators: %s is the post type name.
#: my-custom-post-types.php:263
msgctxt "post type"
msgid "%s scheduled."
msgstr "%s を予約しました。"

#. translators: %s is the post type name.
#: my-custom-post-types.php:265
msgctxt "post type"
msgid "%s updated."
msgstr "%s を更新しました。"

#. translators: %s is the post type name.
#: my-custom-post-types.php:267
msgctxt "post type"
msgid "%s link"
msgstr "%s リンク"

#. translators: %s is the post type slug.
#: my-custom-post-types.php:269
msgctxt "post type slug"
msgid "A link to %s"
msgstr "%s へのリンク"

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:308
msgctxt "taxonomy"
msgid "All %s"
msgstr "%s 一覧"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:310
msgctxt "singular taxonomy"
msgid "Edit %s"
msgstr "%s を編集"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:312
msgctxt "singular taxonomy"
msgid "View %s"
msgstr "%s を表示"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:314
msgctxt "singular taxonomy"
msgid "Update %s"
msgstr "%s を更新"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:316
msgctxt "singular taxonomy"
msgid "Add New %s"
msgstr "新規 %s を追加"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:318
msgctxt "singular taxonomy"
msgid "New %s Name"
msgstr "新規 %s 名"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:320
msgctxt "singular taxonomy"
msgid "Parent %s"
msgstr "親の %s"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:322
msgctxt "singular taxonomy"
msgid "Parent %s:"
msgstr "親の %s:"

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:324
msgctxt "taxonomy"
msgid "Search %s"
msgstr "%s を検索"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:326
msgctxt "singular taxonomy"
msgid "No %s found."
msgstr "%s が見つかりませんでした。"

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:328
msgctxt "taxonomy"
msgid "No %s"
msgstr ""

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:330
msgctxt "singular taxonomy"
msgid "Filter by %s"
msgstr "%s で絞り込む"

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:332
msgctxt "taxonomy"
msgid "%s list navigation"
msgstr "%s リストナビゲーション"

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:334
msgctxt "taxonomy"
msgid "%s list"
msgstr "%s リスト"

#. translators: %s is the taxonomy name.
#: my-custom-post-types.php:336
msgctxt "taxonomy"
msgid "← Back to %s"
msgstr "%s へ戻る"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:338
msgctxt "singular taxonomy"
msgid "%s link"
msgstr "%s リンク"

#. translators: %s is the singular taxonomy name.
#: my-custom-post-types.php:340
msgctxt "singular taxonomy"
msgid "A link to %s"
msgstr "%s へのリンク"

#. translators: %d is the max number for the slug name validation.
#: my-custom-post-types.php:400
msgid "Must not exceed %d characters"
msgstr "%d 文字を超えてはいけません"

MO ファイルを作成

以下を実行して PHP ファイルの翻訳用の MO ファイル(my-custom-post-types-ja.mo)を作成します。

% wp i18n make-mo languages/

.l10n.php(PHP)ファイルを作成

以下を実行して PHP ファイル翻訳用 .l10n.php ファイルを作成します。

WordPress 6.4 までは翻訳ファイルは MO ファイルを読み込んでいましたが、6.5 からは PHP ファイル(.l10n.php)があれば、それを読み込むことでパフォーマンスが改善されるようになっています。

% wp i18n make-php languages/

以下のような .l10n.php ファイル(my-custom-post-types-ja.l10n.php)が生成されます。

1行に記述されているので、コード右上の「wrap」にチェックを入れると見やすいです。

<?php
return ['domain'=>'my-custom-post-types','plural-forms'=>NULL,'language'=>'ja','project-id-version'=>'My Custom Post Types 0.2.2','pot-creation-date'=>'2025-03-09T04:41:44+00:00','po-revision-date'=>'YEAR-MO-DA HO:MI+ZONE','x-generator'=>'WP-CLI 2.11.0','messages'=>['A plugin to create custom post types'=>'カスタム投稿タイプを作成するプラグイン','My Custom Post Type Settings'=>'My Custom Post Type の設定','My Custom Post Types Settings'=>'My Custom Post Types の設定','Custom Post Type Settings'=>'カスタム投稿タイプ設定','Enter the Custom Post Type information'=>'カスタム投稿タイプの情報を入力','Post Type Name (Plural)'=>'投稿タイプ名(複数形)','Post Type Name (Singular)'=>'投稿タイプ名(単数形)','Post Type Slug'=>'投稿タイプスラッグ','Menu Icon'=>'メニューアイコン','Custom Taxonomy Settings'=>'カスタムタクソノミー設定','Enter the Custom Taxonomy information'=>'カスタムタクソノミーの情報を入力','Taxonomy Name (Plural)'=>'タクソノミー名(複数形','Taxonomy Name (Singular)'=>'タクソノミー名(単数形)','Taxonomy Hierarchical'=>'階層構造','Required.'=>'必須','If omitted, it will be generated automatically.'=>'省略時は自動的に生成されます','Dash Icon Name (Optional)'=>'ダッシュアイコンの名前(オプション)','Enable hierarchy'=>'階層構造を有効','Check to enable hierarchy'=>'階層構造を有効化するにはチェックを入れる','post typeAll %s'=>'全ての %s','singular post typeEdit %s'=>'%s を編集','singular post typeView %s'=>'%s を表示','post typeView %s'=>'%s を表示','singular post typeAdd New %s'=>'新規 %s を追加','Add New'=>'新規 %s を追加','singular post typeNew %s'=>'新規 %s','post typeParent %s:'=>'親の %s:','post typeSearch %s'=>'%s を検索','post type slugNo %s found.'=>'%s が見つかりませんでした。','post type slugNo %s found in Trash.'=>'ゴミ箱に %s が見つかりませんでした。','post type%s Archives'=>'%s アーカイブ','post type%s Attributes'=>'%s 属性','post type slugInsert into %s'=>'%s に挿入','post type slugUploaded to this %s'=>'この %s にアップロードされました','post type slugFilter %s list'=>'%s リストを絞り込み','post type slugFilter %s by date'=>'%s を日時で絞り込み','post type%s list'=>'%s リスト','post type%s published.'=>'%s を公開しました。','post type%s published privately.'=>'%s をプライベートで公開しました。','post type%s reverted to draft.'=>'%s を下書きに戻しました。','post type%s scheduled.'=>'%s を予約しました。','post type%s updated.'=>'%s を更新しました。','post type%s link'=>'%s リンク','post type slugA link to %s'=>'%s へのリンク','taxonomyAll %s'=>'%s 一覧','singular taxonomyEdit %s'=>'%s を編集','singular taxonomyView %s'=>'%s を表示','singular taxonomyUpdate %s'=>'%s を更新','singular taxonomyAdd New %s'=>'新規 %s を追加','singular taxonomyNew %s Name'=>'新規 %s 名','singular taxonomyParent %s'=>'親の %s','singular taxonomyParent %s:'=>'親の %s:','taxonomySearch %s'=>'%s を検索','singular taxonomyNo %s found.'=>'%s が見つかりませんでした。','singular taxonomyFilter by %s'=>'%s で絞り込む','taxonomy%s list navigation'=>'%s リストナビゲーション','taxonomy%s list'=>'%s リスト','taxonomy← Back to %s'=>'%s へ戻る','singular taxonomy%s link'=>'%s リンク','singular taxonomyA link to %s'=>'%s へのリンク','Must not exceed %d characters'=>'%d 文字を超えてはいけません']];

これで、languages ディレクトリには以下のように4つの翻訳関連のファイルが作成されます。

my-custom-post-types/
  ├── assets/
  │   └── my-custom-post-types-admin.js
  ├── languages/
  │   ├── my-custom-post-types-ja.l10n.php
  │   ├── my-custom-post-types-ja.mo
  │   ├── my-custom-post-types-ja.po
  │   └── my-custom-post-types.pot
  └── my-custom-post-types.php

この例では、JavaScript も翻訳可能にしましたが、wp_localize_script を使って実質的には PHP の翻訳関数を使っているので、JavaScript ファイル用の翻訳ファイル(JSON フォーマット)は必要はありません。

MO(.l10n.php)ファイルをロード

PHP ファイルの翻訳を適用するため、my-custom-post-types.php で load_plugin_textdomain を使って MO(.l10n.php)ファイルをロードします。

以下では init フックで load_plugin_textdomain を呼び出しています。第1引数には block.json の textdomain の値を、第2引数は false(デフォルト)を指定し、第3引数は MO ファイルや .l10n.php ファイルの保存フォルダへのパスを指定します。

function my_custom_post_types_init() {

  // MO ファイルをロード (追加)
  load_plugin_textdomain( 'my-custom-post-types', false, basename( __DIR__ ) . '/languages' );

  $post_type_name = get_option('post_type_name') ? get_option('post_type_name') : '';
  $post_type_singular_name = get_option('post_type_singular_name') ? get_option('post_type_singular_name') : '';
  $post_type_slug = get_option('post_type_slug') ? get_option('post_type_slug') : sanitize_title($post_type_singular_name);
  $menu_icon = get_option('menu_icon') ? get_option('menu_icon') : 'dashicons-admin-post';

  if (empty($post_type_name) || empty($post_type_singular_name)) {
    return;
  }
  my_custom_post_types_register_post_type($post_type_name, $post_type_singular_name, $post_type_slug, $menu_icon);

  $taxonomy_name = get_option('taxonomy_name') ? get_option('taxonomy_name') : '';
  $taxonomy_singular_name = get_option('taxonomy_singular_name') ? get_option('taxonomy_singular_name') : '';
  $hierarchical =  (bool) get_option('taxonomy_hierarchical');

  if (empty($taxonomy_name) || empty($taxonomy_singular_name)) {
    return;
  }

  my_custom_post_types_register_taxonomy($post_type_slug, $taxonomy_name, $taxonomy_singular_name, $hierarchical);
}
add_action('init', 'my_custom_post_types_init');

上記により、最新の環境(WordPress 6.5 以降)であれば、language ディレクトリの my-custom-post-types-ja.l10n.php がロードされ、翻訳が適用されます。

これで WordPress の言語設定が日本語の場合は、以下のように日本語の翻訳が表示されます。

設定ページ

設定ページ(JavaScript の検証)

カスタム投稿タイプ画面

プラグイン一覧