WordPress 6.5 の翻訳処理の改善をコードレベルで理解しよう

石川英典

プライム・ストラテジー「KUSANAGI」開発チームの石川です。
以前にこのコラムにおいて WordPress 6.5で翻訳処理が高速化 したことを取り上げました。
今回は具体的にどのように変わったのかをコードレベルで解説します。

翻訳処理とは

近年のソフトウェアでは実行している環境の言語 (ロケール localeといいます) に合わせて、表示する言語を変えられるようになっています。
例えば KUSANAGI の RPM の更新に使う yum (あるいは dnf) コマンドはデフォルトでは英語のメッセージに、日本語の環境ではメッセージが日本語で表示されます。
以下は表示の例です。

【デフォルトの場合】
Installed Packages
Name         : kusanagi
Version      : 9.5.2
Release      : 1.el8
Architecture : noarch
Size         : 682 k
Source       : kusanagi-9.5.2-1.el8.src.rpm
Repository   : @System
From repo    : kusanagi
Summary      : KUSANAGI Core
URL          : https://kusanagi.tokyo
License      : GPL-2.0-only
Description  : This package contains KUSANAGI command, backend and core files.

【日本語の場合】
インストール済みパッケージ
名前         : kusanagi
バージョン   : 9.5.2
リリース     : 1.el8
Arch         : noarch
サイズ       : 682 k
ソース       : kusanagi-9.5.2-1.el8.src.rpm
リポジトリー : @System
repo から    : kusanagi
概要         : KUSANAGI Core
URL          : https://kusanagi.tokyo
ライセンス   : GPL-2.0-only
説明         : This package contains KUSANAGI command, backend and core files.

このように表示言語をプログラム上で切り替える仕組みを 翻訳処理 と呼びます。

では、この翻訳処理はどのように実装されているのでしょうか。

ソフトウェアのソースコードではデフォルトの言語 (一般的には英語) でメッセージは記述されます。
このデフォルトの言語に対して、各言語 (今回は日本語を例にします) で対照表を用意します。
これを翻訳ファイルと呼びます。
上記の例で言うと、以下のような内容になります。

  • Installed Packages → インストール済みパッケージ
  • Name → 名前
  • Version → バージョン
  • Release → リリース

日本語で表示を行う場合は、ソースコードに書かれたデフォルトの言語のメッセージを、この翻訳ファイルを参照しながら逐次日本語に置き換えて表示しています。

WordPressの翻訳処理

では、WordPressでの実際の処理をコードで見ていきましょう。
特に断わりがない限りは WordPress 6.5.5 をベースに説明します。

WordPressの文字列がどのように翻訳されるか

例として WordPress のログイン画面である wp-login.php を見てみます。

WordPress のログイン画面

1497行目 がWordPressのログイン画面のフォームのコードになります。
以下はその抜粋です。

		<form name="loginform" id="loginform" action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post">
			<p>
				<label for="user_login"><?php _e( 'Username or Email Address' ); ?></label>
				<input type="text" name="log" id="user_login"<?php echo $aria_describedby; ?> class="input" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required="required" />
			</p>

			<div class="user-pass-wrap">
				<label for="user_pass"><?php _e( 'Password' ); ?></label>

ここで 1499行目 を見てみます。
<?php _e( 'Username or Email Address' ); ?> が翻訳して文字列を表示する処理になります。
_e() が翻訳された文字列を表示するWordPressの関数です。
Username or Email Address を翻訳ファイルを参照して ユーザー名またはメールアドレス に置き換え、ログインフォームに日本語の文字列が表示されるのです。

少し下の 1504行目 では <?php _e( 'Password' ); ?> があり、 Passwordパスワード に置き換えて表示します。

ここではログイン画面を例にしましたが、テーマや管理画面、エラーメッセージでも同様になります。

翻訳を表示する処理の実装

WordPressの翻訳関連の処理は wp-includes/l10n.php にあります。

次に 351行目_e() の処理の中身を見ます。

function _e( $text, $domain = 'default' ) {
    echo translate( $text, $domain );
}

translate() を呼び出していることが分かりました。

次に 193行目translate() の処理の中身を見ます。
以下はその抜粋です。

function translate( $text, $domain = 'default' ) {
    $translations = get_translations_for_domain( $domain );
    $translation  = $translations->translate( $text );

194行目get_translations_for_domain() が翻訳ファイルを取得する処理です。
対応する翻訳ファイルのクラス (厳密にはファイルをカプセル化したクラス) である WP_Translations クラスのインスタンスとして返ります。

195行目 でそのインスタンスの $translations->translate() を呼び出し、対応する翻訳された文字列を取得するのです。

先のログイン画面を例にすると、 Username or Email Address$text に渡して $translations->translate() を実行して $translationユーザー名またはメールアドレス が返ります。

翻訳ファイルの読み込み処理

次に翻訳ファイル (をカプセル化したクラス) を見ていきます。

さきほどの翻訳ファイルを取得した get_translations_for_domain() の処理の中身を 1398行目 から見ます。

function get_translations_for_domain( $domain ) {
    global $l10n;
    if ( isset( $l10n[ $domain ] ) || ( _load_textdomain_just_in_time( $domain ) && isset( $l10n[ $domain ] ) ) ) {
        return $l10n[ $domain ];
    }

    static $noop_translations = null;
    if ( null === $noop_translations ) {
        $noop_translations = new NOOP_Translations();
    }

    $l10n[ $domain ] = &$noop_translations;

    return $noop_translations;
}

$l10n は WordPress の翻訳ファイルを保持しているグローバル変数です。この配列から取り出しているということが分かります。

では、その翻訳ファイルはどのように読み込まれたのでしょうか。

725行目load_textdomain() が実体になります。
以下が抜粋です。

function load_textdomain( $domain, $mofile, $locale = null ) {
(省略)
    $plugin_override = apply_filters( 'override_load_textdomain', false, $domain, $mofile, $locale );

    if ( true === (bool) $plugin_override ) {
        unset( $l10n_unloaded[ $domain ] );

        return true;
    }
(省略)
    $i18n_controller = WP_Translation_Controller::get_instance();

    // Ensures the correct locale is set as the current one, in case it was filtered.
    $i18n_controller->set_locale( $locale );
(省略)
    $preferred_format = apply_filters( 'translation_file_format', 'php', $domain );
    if ( ! in_array( $preferred_format, array( 'php', 'mo' ), true ) ) {
        $preferred_format = 'php';
    }

    $translation_files = array();

    if ( 'mo' !== $preferred_format ) {
        $translation_files[] = substr_replace( $mofile, ".l10n.$preferred_format", - strlen( '.mo' ) );
    }

    $translation_files[] = $mofile;

    foreach ( $translation_files as $file ) {
(省略)
        $success = $i18n_controller->load_file( $file, $domain, $locale );

        if ( $success ) {
            if ( isset( $l10n[ $domain ] ) && $l10n[ $domain ] instanceof MO ) {
                $i18n_controller->load_file( $l10n[ $domain ]->get_filename(), $domain, $locale );
            }

            // Unset NOOP_Translations reference in get_translations_for_domain().
            unset( $l10n[ $domain ] );

            $l10n[ $domain ] = new WP_Translations( $i18n_controller, $domain );

            $wp_textdomain_registry->set( $domain, $locale, dirname( $file ) );

            return true;
        }
    }

    return false;
}

815行目translation_file_format は WordPress 6.5 より追加された新しい hook です。
以前のコラムgettext からPHPコードへ翻訳の仕組みを変更したことを紹介しました。
この hook が設定されていなければ、817行目 でPHPコードの仕組みである 'php' が選択されます。
また 'mo' を指定すると、従来の gettext の仕組みが選択されるようになります。

実際のファイル読み込みは 842行目$i18n_controller->load_file() で行われます。
ここは foreach ループの中になっていることがポイントです。
PHPコードの仕組みは新しい機能であるため、全ての翻訳ファイルが対応しているわけではありません。
WordPress 6.5.5 の初期インストールで存在を確認できるファイルは以下のみです。

  • wp-content/languages/admin-ja.l10n.php
  • wp-content/languages/admin-network-ja.l10n.php
  • wp-content/languages/continents-cities-ja.l10n.php
  • wp-content/languages/ja.l10n.php
  • wp-content/languages/plugins/akismet-ja.l10n.php

一方で gettext の仕組みの翻訳ファイルはこれだけあります。
特にテーマファイルのPHPコード翻訳が存在していません。(2024-07-16現在)

  • wp-content/languages/admin-ja.mo (PHPコードあり)
  • wp-content/languages/admin-network-ja.mo (PHPコードあり)
  • wp-content/languages/continents-cities-ja.mo (PHPコードあり)
  • wp-content/languages/ja.mo (PHPコードあり)
  • wp-content/languages/plugins/akismet-ja.mo (PHPコードあり)
  • wp-content/languages/plugins/hello-dolly-ja.mo
  • wp-content/languages/themes/twentytwentyfour-ja.mo
  • wp-content/languages/themes/twentytwentythree-ja.mo
  • wp-content/languages/themes/twentytwentytwo-ja.mo

そのため、'php' が選択されていたとしても、存在しなかった場合には 'mo' を代わりに読み込むようにする必要があります。
foreach ループにして、まず 'php' の読み込みが正常に終了した場合は return true; することで、読み込みできた時点で処理を終えるようにしているのです。

WordPress 6.5以前の翻訳を表示する処理の実装

さて、参考までに高速化する前の WordPress 6.4.5 の処理を見てみましょう。

716行目load_textdomain() から抜粋します。

function load_textdomain( $domain, $mofile, $locale = null ) {
(省略)
    $plugin_override = apply_filters( 'override_load_textdomain', false, $domain, $mofile, $locale );

    if ( true === (bool) $plugin_override ) {
        unset( $l10n_unloaded[ $domain ] );

        return true;
    }
(省略)
    $mo = new MO();
    if ( ! $mo->import_from_file( $mofile ) ) {
        $wp_textdomain_registry->set( $domain, $locale, false );

        return false;
    }

    if ( isset( $l10n[ $domain ] ) ) {
        $mo->merge_with( $l10n[ $domain ] );
    }

    unset( $l10n_unloaded[ $domain ] );

    $l10n[ $domain ] = &$mo;

    $wp_textdomain_registry->set( $domain, $locale, dirname( $mofile ) );

    return true;
}

791行目MO クラスのインスタンスを作成し、 792行目$mo->import_from_file()gettext の仕組みで翻訳ファイルを読み込んでいます。

また、今回は説明を省きましたが、WordPress 6.4 では $l10nMO クラスのインスタンスを直接格納していましたが、WordPress 6.5 では 'mo''php' の両方に対応するために双方をラップした WP_Translations クラスのインスタンスを格納する形に変更されています。

このように、従来は gettext の仕組みのみであったものが、 WordPress 6.5 より 'mo''php' の両方に対応するように変わったということが分かります。

KUSANAGIの翻訳アクセラレーターとWordPress 6.5の翻訳処理の関係

従来より KUSANAGI専用プラグイン には WordPress の翻訳処理を高速化するための仕組みとして 翻訳アクセラレーター があります。

WordPressの文字列がどのように翻訳されるか で解説したとおり、WordPress で翻訳に対応している文字列全てにおいて、翻訳された文字列に置換する処理が行われるため、メモリの使用量とパフォーマンスに影響があることは以前から明らかでした。
twentytwentyfourのように WordPress がデフォルトで用意しているテーマは日本語のみならず、様々な言語の翻訳が用意されています。これらを利用して様々な言語のユーザーが利用するサイトを運用する場合は、翻訳処理を行う必要があります。
しかし、日本語のサイトのみを運用している場合はテーマをそもそも日本語でベタ書きしていることがほとんどであると思います。
こういったサイトでは必要のない翻訳ファイルを読み込むのは無駄な処理であると言えます。
そこで翻訳アクセラレーターでは、この 「翻訳を停止」 することで高速化する仕組みを用意しています。
翻訳アクセラレーターでは load_textdomain()768行目 にある override_load_textdomain hook を利用して翻訳ファイルの処理をバイパスすることで、処理を高速化しているのです。
一般のサイトについては、翻訳アクセラレーターのデフォルトは 「翻訳を停止」 になっています。

なお、ログイン/サインアップ画面と管理画面ではデフォルトは 「キャッシュを使用」 するようになっています。これは翻訳ファイルの処理は行うものの、毎回ファイルを読み込まずにキャッシュするものです。
翻訳アクセラレーターのキャッシュは APC (PHP 8以降では APCu) を使用してメモリに保持します。
翻訳アクセラレーターを開発した当時、Webサーバのストレージはハードディスクが一般的で、 'mo' のファイルを読み込む処理がボトルネックになっていたからです。
しかし、近年のWebサーバではストレージがSSDになっていることもあり、APC を使うよりもファイルから読み込んでも性能差が出難くなっています。特に WordPress 6.5 以降の 'php' ではPHPコードをキャッシュする OPcache の恩恵を受けることもできます。
そこで、今後はログイン/サインアップ画面と管理画面では 「通常翻訳」 (翻訳アクセラレーターを使わないWordPressの翻訳処理を指します) に変更することをおすすめします。
ただし、ログイン/サインアップ画面と管理画面を 「翻訳を停止」 にすると表示が全て英語 (デフォルトの言語) になってしまいますので注意してください。

<< regreSSHionというOpenSSHの脆弱性について徹底解説Azure DevOpsでAzure Kubernetesへの自動リリースとデプロイ >>

関連記事

Webサイト運用の課題解決事例100選 プレゼント

Webサイト運用の課題を弊社プロダクトで解決したお客様にインタビュー取材を行い、100の事例を108ページに及ぶ事例集としてまとめました。

・100事例のWebサイト運用の課題と解決手法、解決後の直接、間接的効果がわかる

・情報通信、 IT、金融、メディア、官公庁、学校などの業種ごとに事例を確認できる

・特集では1社の事例を3ページに渡り背景からシステム構成まで詳解