プライム・ストラテジー「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
を見てみます。
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 では $l10n
に MO
クラスのインスタンスを直接格納していましたが、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の翻訳処理を指します) に変更することをおすすめします。
ただし、ログイン/サインアップ画面と管理画面を 「翻訳を停止」 にすると表示が全て英語 (デフォルトの言語) になってしまいますので注意してください。