WordPressの翻訳用のファイルがどうやって読み込まれているのかを調べてみた

追記: 2016年9月20日

追記(2016年9月20日)ここまで


WordPressは、世界中で使われていますが、翻訳ファイルと翻訳者のおかげで、コアファイルを触ることなく、色々な言語で使うことができてとっても便利!

テーマやプラグインの制作者が、翻訳ファイルなどの仕組みをちゃんと整えて、色々な国の言葉に簡単に翻訳できるようにすることを国際化、逆に、そうした仕組みを利用して翻訳を行うことを地域化といいます。

ちなみに国際化は、internationalizationinの間に18文字あることからi18nと書き、地域化はlocalizationで、lnの間に10文字あることからL10nと書きます(Lは小文字にすると数字の1と見間違えるので大文字)。面白いですよね。

テーマやプラグインの国際化や地域化の具体的で詳しくて分かりやすくて自分もやってみたくなってしまうやり方・方法については、2014年7月18日に発売されることになったプラグインの開発方法を説明する書籍に書かれています(宮内さん岡本さん三好さんと僕の共著です)。

今回は、ちょっとソースを見て、翻訳ファイルがどういうルールで読み込まれているのかを調べてみましたので、共有したいと思います。

テキストドメイン、ロケール、パス

前提として、テキストドメインとロケールとパスについて。

 日本語  英語  内容
 テキストドメイン  textdomain  翻訳すべき文字列を取得するために必要な、テーマやプラグインごとにユニークな識別用のIDみたいなもの
 ロケール  locale  en_USは英語でアメリカ、jaは日本語、といったように言語と地域を組み合わせたもの。WPLANGで指定するのでおなじみ。
 パス  path  翻訳ファイルがどこにあるのかを示します。

WordPressは上記の3つを使って、翻訳ファイルを読み込んで翻訳します。プラグインで言うと、

  1. textdomain: どのプラグイン用の、
  2. locale: 何語用のファイルが、
  3. path: どこに置いてあるのか

がWordPressに伝わればいいということになります。このうち、localeについては、wp-config.phpファイルに書かれている、WPLANGの値(指定がなければen_US)が使われます。

プラグイン用の書き方を例に、基本的な動き

プラグインを国際化するためには、以下のload_plugin_textdomainを使います。

load_plugin_textdomain( $domain, $abs_rel_path, $plugin_rel_path )

2つ目の引数は非推奨になったけど消すこともできずに残っている切ないものなので、falseにしておきます。

book-stealthというプラグインを作っているとすると、以下のようにプラグインの中に書きます(フックの仕方も含めて)。

add_action( 'plugins_loaded', 'book_stealth_load_textdomain' );
function book_stealth_load_textdomain() {
    load_plugin_textdomain( 'book-stealth', false, dirname( plugin_basename( __FILE__ ) ) . '/languages/' );
}

一方、プラグインの中で翻訳したい文字列は翻訳関数の__()や_e()などを使って、<?php _e( ‘This is a nice book!’, ‘book-stealth’ ); ?>というふうに書いておきます。

content_php
これはテーマの場合だけど、こんな感じで__()や_e()を使って書く。

そして、翻訳ファイルには、’This is a nice book!’という翻訳されるべき文字列(英語です)と’これはスゴイ本だなぁ!’という翻訳のペアが、たくさん並べておきます。

TwentyTwelveの翻訳ファイルをPoeditという翻訳ファイル編集ソフトで開いているところ。
TwentyTwelveの翻訳ファイルをPoeditという翻訳ファイル編集ソフトで開いているところ。

なので、WordPressは、

  1. book-stealthというテキストドメインを持つ翻訳を探そう
  2. locale: WPLANGがjaだからbook-stealth-ja.moだな
  3. path: プラグインディレクトリの中のlanguagesフォルダにあるのね

という風にして、翻訳ファイルの読み込みと翻訳をしてくれます。

テーマの場合は以下のようになります。

add_action( 'after_setup_theme', 'my_theme_setup' );
function my_theme_setup(){
    load_theme_textdomain( 'my_theme', get_template_directory() . '/languages' );
}

 読み込みの順序

さて、指定された場所に翻訳ファイルがなかった場合にはどうなるのか、が今回調べたことです。

プラグインの場合

さきほどのプラグインの例では、日本語であれば/wp-content/plugins/book-stealth/languages/book-stealth-ja.moを探すわけですが、これがない場合はどうなるか。/wp-includes/l10n.phpにある、load_plugin_textdomain()を見ると(コメント訳した)、

function load_plugin_textdomain( $domain, $deprecated = false, $plugin_rel_path = false ) {

	// WPLANGからlocaleを取得
	$locale = get_locale();
	$locale = apply_filters( 'plugin_locale', $locale, $domain );

	// path を以下の順で決める。
	// 1. 第三引数で指定された場所
	// 2. 非推奨を伝えつつ、第二引数も見てあげる
	// 3. なければpluginsディレクトリ
	if ( false !== $plugin_rel_path	) {
		$path = WP_PLUGIN_DIR . '/' . trim( $plugin_rel_path, '/' );
	} else if ( false !== $deprecated ) {
		_deprecated_argument( __FUNCTION__, '2.7' );
		$path = ABSPATH . trim( $deprecated, '/' );
	} else {
		$path = WP_PLUGIN_DIR;
	}

	// さっそく読んでみる
	$mofile = $domain . '-' . $locale . '.mo';
	// load_textdomain()は実際に読み込んで$l10nというグローバル変数に$l10n[$domain]をセットして成功すればtrueを帰す
	if ( $loaded = load_textdomain( $domain, $path . '/'. $mofile ) )
		return $loaded;

	// 読めなかったら、/wp-content/languages/plugins/book-stealth-ja.moを探してみる
	$mofile = WP_LANG_DIR . '/plugins/' . $mofile;
	return load_textdomain( $domain, $mofile );
}

となっていますので、以下の順です。

  1.  第三引数が普通に渡されていれば、そこ。上の例では、/wp-content/plugins/book-stealth/languages/book-stealth-ja.mo
  2. 第二引数が渡されていれば、そこ
  3. なければ、/wp-content/plugins/book-stealth-ja.mo
  4. それでもなければ、/wp-content/languages/plugins/book-stealth-ja.mo

となります。WordPress日本語版をインストールすると、akismetの翻訳ファイルが/wp-content/languages/plugins/に入っているのですが、この4番目を利用していたということだったんですね。

akismetの翻訳ファイルの在処
akismetの翻訳ファイルの在処

なるほど〜。

テーマの場合

テーマの場合もだいたい同じです。引数は、load_theme_textdomain( $domain, $path = false )。順番は、以下です。

  1. 第二引数が普通に渡されていれば、/wp-content/themes/theme-name/languages/ja.mo
  2. なければ、/wp-content/themes/theme-name/ja.mo
  3. それでもなければ、/wp-content/languages/themes/theme-name-ja.mo

注意点は、wp-content/languages/themes/に入れるときはテキストドメインをファイル名に入れておかないといけないこと。当たり前ですけど。

ソースは以下で、面白いのは子テーマを作っている時の動きだと思いました。

function load_theme_textdomain( $domain, $path = false ) {

    $locale = get_locale();
    $locale = apply_filters( 'theme_locale', $locale, $domain );

    // 1. 指定された場所
    // 2. 親テーマのディレクトリ
    if ( ! $path )
        $path = get_template_directory();

    // 読んでみる
    $mofile = "{$path}/{$locale}.mo";
    if ( $loaded = load_textdomain( $domain, $mofile ) )
        return $loaded;

    // それでもなければ/wp-content/languages/themes/book-stealth-ja.moを探してみる
    $mofile = WP_LANG_DIR . "/themes/{$domain}-{$locale}.mo";
    return load_textdomain( $domain, $mofile );
}

パスが指定されていればそれが読まれますが、パスが指定されていない場合、親テーマのja.moを探しに行っています(get_template_directory())。ということは、子テーマでパスまで指定してやれば、親テーマの翻訳に追加ができるということですね。

親テーマと子テーマから同じテキストドメインを指定?

親テーマにもload_theme_textdomainがあり、子テーマでもload_theme_textdomainがあり、両方から同じテキストドメインで読み込んだ場合には、翻訳が被ってしまうのだけど、後述するように、2つのファイルがマージされます。先に読まれたほうが採用されるようなので、多分、子テーマの方が先に読まれます。

また、テーマよりもプラグインの方が先に読まれますので、プラグインから指定することもできます。

それと、これはtwentyfourteenなどのデフォルトテーマシリーズの場合ですが、翻訳ファイルは<code>/wp-content/languages/themes/twentyfourteen-ja.mo</code>にあるので、子テーマでload_theme_textdomain()をしてやれば、翻訳を上書きできるということですね。デフォルトの翻訳ファイルをコピーしてきて編集したものを、子テーマ内に置いておけば、親テーマがアップデートされたとしても大丈夫だ、という工夫なのかな。

 最後にload_textdomain( $domain, $mofile )について

というわけで、プラグインとテーマでの読み込みの順番についてみてみました。読み込みを実際に行っているload_textdomainを見て終わりにします。

ソースのコメントの翻訳から。

/**
* Load a .mo file into the text domain $domain.
* .moファイルを読み込む。(テキストドメインの中に?)
*
* If the text domain already exists, the translations will be merged. If both
* sets have the same string, the translation from the original value will be taken.
* もし、テキストドメインがすでにあったら、翻訳はマージされます。
* もし、被ったらオリジナルのほうが取られます。
*
* On success, the .mo file will be placed in the $l10n global by $domain
* and will be a MO object.
* 成功したら、.moファイルはグローバル変数$l10nに$domain付きで置かれて、MOオブジェクトになる
*
* @since 1.5.0
*
* @param string $domain Text domain. Unique identifier for retrieving translated strings.
* @param string $mofile Path to the .mo file.
* @return bool True on success, false on failure.
*/

$moオブジェクトというのがあるのですね。以下の定義を見ると、作成された$moオブジェクトは、$l10nというグローバル変数にテキストドメインをキーとして格納されるようです。

読み込みの順番で面白いと思ったのは、41行目で読み込むファイルを変えることができる点です。

プラグインの翻訳ファイルを、自分の環境だけで変えて、アップデートしてもそれを持ち続けたい、というような場合には、ここで差し替えることができそうです。

function load_textdomain( $domain, $mofile ) {
	global $l10n;

	/**
	 * textdomainやmoファイルパスを上書きするかどうかをしている
	 * Filter text domain and/or MO file path for loading translations.
	 *
	 * @since 2.9.0
	 *
	 * @param bool   $override Whether to override the text domain. Default false.
	 * @param string $domain   Text domain. Unique identifier for retrieving translated strings.
	 * @param string $mofile   Path to the MO file.
	 */
	$plugin_override = apply_filters( 'override_load_textdomain', false, $domain, $mofile );

	// 上書きするなら返しちゃう。別途自分で読み込まないといかないかな?
	if ( true == $plugin_override ) {
		return true;
	}

	/**
	 * ここでアクションをひとつ
	 * Fires before the MO translation file is loaded.
	 *
	 * @since 2.9.0
	 *
	 * @param string $domain Text domain. Unique identifier for retrieving translated strings.
	 * @param string $mofile Path to the .mo file.
	 */
	do_action( 'load_textdomain', $domain, $mofile );

	/**
	 * 読み込むべきmoファイルをフィルターできる
	 * Filter MO file path for loading translations for a specific text domain.
	 *
	 * @since 2.9.0
	 *
	 * @param string $mofile Path to the MO file.
	 * @param string $domain Text domain. Unique identifier for retrieving translated strings.
	 */
	$mofile = apply_filters( 'load_textdomain_mofile', $mofile, $domain );

	// 読めないときはfalseが返る
	if ( !is_readable( $mofile ) ) return false;

	// MOクラスのインスタンス。MOクラス、/wp-includes/pomo/mo.phpにある
	$mo = new MO();

	// 読めなかったらfalse
	if ( !$mo->import_from_file( $mofile ) ) return false;

	// すでに同じtextdomainがあった場合にはマージ
	if ( isset( $l10n[$domain] ) )
		$mo->merge_with( $l10n[$domain] );

	// textdomainをキーにした配列として$l10nに格納して
	$l10n[$domain] = &$mo;

	// trueを返す
	return true;
}

以下は、$l10nをダンプしてみた例

array(2) {
["twentytwelve"]=>
 &object(MO)#184 (4) {
  ["_nplurals"]=>
  int(1)
  ["entries"]=>
  array(72) {
   ["% Replies"]=>
   object(Translation_Entry)#1705 (9) {
    ["is_plural"]=>
    bool(false)
    ["context"]=>
    NULL
    ["singular"]=>
    string(9) "% Replies"
    ["plural"]=>
    NULL
    ["translations"]=>
    array(1) {
     [0]=>
     string(13) "%件の返信"
    }
    ["translator_comments"]=>
    string(0) ""
    ["extracted_comments"]=>
    string(0) ""
    ["references"]=>
    array(0) {
    }
    ["flags"]=>
    array(0) {
    }
   }
 }
 // 他にも翻訳されるべき文字列をキーにした配列がたくさん。
}
// 他にもテキストドメインをキーにしたMOオブジェクトが配列として格納されてる
}

↓ プラグインを作る方々への本、書きました。 ↓

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

WordPressの翻訳用のファイルがどうやって読み込まれているのかを調べてみた」への2件のフィードバック