現在、自分のプラグインのドキュメンテーションをしているのだが、将来的には今書いているドキュメンテーション系のコンテンツは多国語化しようと思っている。
では、いざ多国語化しようとする時に、実際にはどうやってやろうか…と、ふと疑問がわいた。

投稿自体を多国語化するプラグインなどは多いので、それらを使っても良いのだが、過去の経験上、それらのプラグインはどれもサイトのパフォーマンスを低下させてしまううえに、現在と同じ分の多国語化した投稿を作らなければならず、作業コスト的にも使いたくない。かと言って、テーマ側で多国語化に対応しようと思っても、基本的にページのコンテンツとなる投稿以外のテンプレート部分しか対応はできない。

私的には「後からでも簡単にコンテンツを多国語化できるような」かなり都合の良いことをやりたいのだ。

色々と考えていたら、一つの方法をひらめいた。

翻訳したいコンテンツを後からショートコードで囲ってやると、そのコンテンツが多国語化候補にできる

──というようなやり方だ。
それほど斬新な発想でもないのだが、まぁ、後付けで多言語化する時の作業コストは低い方なので、やってみる価値はあるかもしれない…。

──で、ドキュメンテーション作業の息抜き(笑)も兼ねて早速そのショートコードを作ってみた次第。

ショートコードの本体と使い方

下記のソースを利用しているテーマのfunctions.phpなどに追加してあげればショートコードが利用できるようになる。

/**
 * Convert to object or array from string like hash
 *
 * @param string $like_hash [required] String of the concatnation of between key and value at the double colon; it should be the comma-delimited if there is multiple
 * @param string $return_type [optional] String for return value specify whether the assoc array or the object; default is defined of "array"
 * @return mixed Return the value of specified type if it completed conversion, return false otherwise
 */
function strtohash( $like_hash=null, $return_type='array' ) {
  if ( empty( $like_hash ) || ! in_array( strtolower( $return_type ), array( 'array', 'object' ) ) ) 
    return false;

  if ( empty( $_tmp_array = explode( ',', $like_hash ) ) ) 
    return false;

  $_assoc = [];
  foreach ( $_tmp_array as $_concat_str ) {
    if ( strpos( $_concat_str, ':' ) !== false ) {
      list( $_key, $_val ) = explode( ':', $_concat_str );
      $_key = trim( trim( stripcslashes( trim( $_key ) ), "\"' " ) );
      $_val = trim( trim( stripcslashes( trim( $_val ) ), "\"' " ) );
      if ( ! empty( $_key ) && strlen( $_key ) > 0 ) {
        $_assoc[$_key] = $_val;
      }
    } else {
      $_concat_str = trim( trim( stripcslashes( trim( $_concat_str ) ), "\"' " ) );
      $_assoc[] = $_concat_str;
    }
  }

  if ( empty( $_assoc ) ) 
    return false;

  if ( 'object' === strtolower( $return_type ) ) 
    return (object) $_assoc;

  return $_assoc;

}

/**
 * Quick translate of content and render that
 *
 * @param array $attributes [require] Array of attributes in shortcode
 * @param string $content [optional] For default is empty
 * @return string $html_content The formatted content
 **/
function custom_quick_translate() {
  list( $attributes, $content ) = func_get_args();
  extract( shortcode_atts( [
    'wrap' => '', // [optional] String of wrapping tag name
    'attributes' => '', // [optional] String like hash for adding attributes in tag (if it wrap at tag)
    'text_domain' => '', // [optional] String of text domain name for a multi-language of the theme you are using
    'strip_tags' => true, // [optional] Specifies by boolean whether to remove tags from wrapped content; Note: it will be removed p tag that wordpress automatically inserted even if you specified the false
  ], $attributes ) );
  $shortcode_name = '_e';
  $render_html = '%s';

  if ( ! empty( $wrap ) ) {
    $_atts = ! empty( $attributes ) ? strtohash( $attributes ) : array();
    $_render_atts = array();
    if ( ! empty( $_atts ) && is_array( $_atts ) ) {
      foreach ( $_atts as $_key => $_val ) {
        $_render_atts[] = esc_attr( $_key ) .'="'. esc_attr( $_val ) .'"';
      }
    }
    $render_html = '<'. $wrap .' '. implode( ' ', $_render_atts ) .'>%s</'. $wrap .'>';
  }

  if ( empty( $text_domain ) ) 
    $text_domain = defined( 'TEXT_DOMAIN' ) ? TEXT_DOMAIN : '';

  $text_domain = apply_filters( 'quick_translate_text_domain', $text_domain, $shortcode_name, $content );

  $content = str_replace( '</p>', "\n", $content );
  $content = str_replace( '<p>', '', $content );
  if ( wp_validate_boolean( $strip_tags ) ) 
    $content = strip_tags( $content );
  $content = str_replace( array( "\r\n", "\r", "\n" ), '\n', $content );

  return sprintf( $render_html, str_replace( '\n', '<br>', __( $content, $text_domain ) ) );
}
add_shortcode( '_e', 'custom_quick_translate' );

使い勝手も考慮して、ショートコード名は極力短く[_e]にしてみた。
なお、前半のstrtohash()関数はショートコードの属性値で渡されたハッシュ型文字列をパースして配列化するユーティリティ関数だ。実はこっちの関数の方が色々なシーンで使える優れものな関数だったりするのだが、今回の記事からは外れるので詳しい説明は省略する。

そして、肝心の使い方だが、いたってシンプルで、翻訳したいコンテンツをこのショートコードで囲ってやればよい。こんな感じだ。

翻訳したいテキストなど

実際の投稿編集画面での使用例は下図を参照してほしい。

ショートコードの使用例

なお、囲われたコンテンツをHTMLタグでラップしたい時などは次のように指定する。

翻訳されるリンクテキスト

これがWEBサイト側へ出力されると、

<a href="//www.example.com/" target="_blank" ref="no_follow">翻訳されるリンクテキスト</a>

──とタグにラップされるのだ。

なお、注意点がある。WordPressが投稿の本文に自動挿入する<p>タグの取り扱いについてだ。
WordPressでは、投稿編集画面で本文に改行を入れると、直前の改行からその部分までが段落となり、自動的に<p>タグで囲われる仕様だ。そのため、改行を含む投稿を[_e]で囲うと編集画面では見えない<p>タグも囲われていることになる。[_e]のショートコードでは文章中に自動挿入されたタグがあるとうまく翻訳が適応されないため、<p>タグを強制的に除去している。もし、翻訳後も本文内の段落を<p>タグで囲いたい場合、今のところ段落ごとに[_e wrap=”p”]で囲うようにするしかない。

ただ、このままだとショートコードに囲まれたテキストの翻訳はされない(下図参照)。

ショートコードの使用直後のサイト表示

今はこれでいいのだが、実際に多国語化する時(つまり後々)にはいくつかの手順を踏む必要がある。

後付けで多国語化をする

1. テキストドメイン名を指定する

多国語化のための翻訳テキストは利用しているテーマの言語翻訳ファイル(.mo)を利用するのが簡単なので、まずは利用しているテーマのテキストドメイン名をショートコード用に定義してあげる必要がある。テキストドメイン名というのはテーマのテンプレートなどの翻訳テキスト部分で__( 'Translate Text', 'twentysixteen' )のように使われている第2引数のtwentysixteenの文字列だ。
わからない時はテーマフォルダ直下にあるstyle.cssのヘッダーコメント内に、

Text Domain: twentysixteen

──のように定義されているので、簡単に確認できる。
このテキストドメイン名をショートコードに設定するには、ショートコードのtext_domain属性にテキストドメイン名を指定する、

翻訳したいテキストなど

──という方法がある。しかし、これだと記述が長くなってしまってショートコードを追加する作業コストがかさむので、テーマ側に定数として指定してしまうのが一番簡単である。
つまり、利用しているテーマのfunctions.phpなどの中に、

if ( ! defined( 'TEXT_DOMAIN' ) ) 
  define( 'TEXT_DOMAIN', 'twentysixteen' );

──と定数を定義してあげるのだ。これでショートコード側でこの定数をテキストドメインとして使用してくれるようになる。
もし、既に同名の定数が定義済みだったりして定数指定が難しいようであれば、フィルターを追加することでも対応できる。その場合は、

function filter_my_text_domain( $text_domain ) {
  $text_domain = 'twentysixteen';
  return $text_domain;
}
add_filter( 'quick_translate_text_domain', 'filter_my_text_domain' );

──とフィルターを追加すればOKだ。

2. 翻訳したいコンテンツ用の多言語化リストを作成する

翻訳ファイル(.mo)を生成する方法は色々あるが、原文テキストと対訳を定義した.poファイルを作成し、そこから.moファイルを生成するという方法が最も一般的だろう。私の場合、Poeditというアプリケーションでそれらの作業を行っているので、ここではPoeditで.moファイルを作成する前提で手順を紹介していく。

まず大抵のテーマには多言語用の翻訳ファイルを格納するlanguagesのようなフォルダがあるはずで、その中には翻訳ファイルのテンプレートである.potファイルが入っている。.potファイルがあれば(なければ.poファイルでも良い)、一度テキスト・エディッターかPoeditで開いてみると、そのテーマで多言語化対象になっている翻訳候補のテキストが定義されていることが確認できるはずである。
しかし、今回新たに[_e]のショートコードで囲われたコンテンツはこの翻訳候補に入っていないので、それを追加してやらないとPoeditで編集ができないのだ。

そこで、テーマのフォルダ直下にextra_translate_text.phpというPHPファイルを作成する。特にファイル名に縛りはないので何でも構わない。
作成したextra_translate_text.phpの中には、[_e]のショートコードで囲ったコンテンツを翻訳候補として下記のように定義していく。

<?php
$_domain = 'twentysixteen';
$my_content = array(
  __( '翻訳したいテキストなど', $_domain ), 
  __( '翻訳されるリンクテキスト', $_domain ), 
  ...
);

改行を含むテキストを翻訳する場合は、改行部分を\nに置き換えてやる必要がある。

__( 'この文章には改行が含まれています。\n注意してください。', $_domain ), 

なお、<p>タグ以外でHTMLタグを含んだテキストを翻訳リストに追加することも可能だ(<p>タグのみはショートコードの方で自動除去されてしまう)。ただし、その場合はショートコードの方でタグを除去しないようにあらかじめ[_e strip_tags=”false”]と指定をしておく必要がある。

__( 'HTMLの<em>タグ</em>が含まれているテキストも翻訳可能です。', $_domain ), 

つまりは、投稿編集画面で本文をショートコード[_e]で囲ったら、囲った部分を__( '{囲った部分}', $_domain )として定義していくということだ。
ちょっと手間はかかるが、翻訳だけ後付けできるのはやはりメリットは大きい。

3. 翻訳ファイル(.moファイル)を作成する

翻訳候補の定義が終わったら、Poeditで.potを開いて、「翻訳プロジェクトを新規作成する」から翻訳する言語を指定して.poファイルを作成する(一度保存すること)。

翻訳プロジェクトを新規作成する

※ もし.potがない場合は、既にある.poファイルを開いて編集することになるので、翻訳プロジェクトを作成する必要はない。

.poが作成できたらカタログの設定を行う。「カタログ」メニュー内の「設定」から「翻訳の設定」と「ソースの検索パス」と「ソース中のキーワード」を適宜変更して保存する(下図の設定例を参照)。

翻訳の設定
ソースの検索パス
ソース中のキーワード

設定が終わったら、「更新する」(もしくは「カタログ」メニュー内の「ソースから更新」)を実行してソースからの同期を行う。
同期が実行されると、先ほどextra_translate_text.phpに追加したコンテンツが翻訳候補としてPoeditのソーステキスト欄に表示されるので、翻訳を行って「保存」すれば、晴れて投稿本文のコンテンツが翻訳するための.moファイルが生成される。

翻訳の編集

4. 多言語化の確認

さて、多言語化の確認をするためにはサイトの言語設定を変更する必要がある。
この設定変更は管理画面の「設定」メニューのサブメニュー「一般」から行える。下図のように、作成した翻訳ファイルの言語コードに合わせて、設定を変更してから翻訳対象コンテンツがあるWebページにアクセスしてみればよい。

サイトの言語設定

この状態で、翻訳コンテンツが含まれているページをブラウザで閲覧してみると、翻訳が反映されていることがわかる。

実例

WordPressの投稿内容

実例 - 投稿内容

extra-translate-text.phpの定義内容

<?php
$_domain = 'twentysixteen';
$my_content = array(
  __( 'Headline', $_domain ), 
  __( 'Text of the line to the paragraph', $_domain ), 
  __( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed pulvinar neque sed magna malesuada, non imperdiet sapien facilisis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. In non iaculis urna. Morbi rutrum urna at eros mattis, at maximus ante finibus. Pellentesque ut arcu justo. Quisque at mi eu erat consequat placerat vitae ut nunc.\\n\\nNulla placerat tortor eget pharetra dignissim. Etiam mollis justo luctus felis ullamcorper congue. Etiam sed maximus purus. Duis justo sapien, cursus non massa quis, hendrerit rhoncus nulla. Duis finibus scelerisque dui quis porttitor. Nam pretium enim ut lacinia blandit. Nulla pharetra porta lectus non accumsan. Nam lacinia lacus id dui accumsan, non vestibulum quam elementum. Etiam hendrerit velit mi, nec efficitur sem dapibus non. Praesent porttitor velit sed libero pulvinar, sed malesuada purus bibendum. Nulla dapibus quam elementum blandit venenatis. Nunc tincidunt odio in lacus semper dapibus. Praesent dui velit, hendrerit vitae magna auctor, lacinia eleifend turpis.', $_domain ) 
);

翻訳ソースファイル(.poファイル)

実例 - 翻訳登録

Webサイト側のページ表示

サイトの言語: 日本語

実例 - 翻訳反映(日本語)
サイトの言語: 英語

実例 - 翻訳反映(英語)

まとめ

最終的に期待通りの動きをしてくれて、なかなかいい感じだった…。
元から多国語化を想定している投稿の本文はあらかじめ[_e]~[/_e]で囲っておくと、翻訳の後付けがよりやりやすくなって良いかもしれない。

ただ、本文のタイトルとかはテーマのテンプレート側で出力時に__( $post->post_title, $text_domain )の処理を追加してあげるか、もしくは下記のフィルターを追加する必要がある。

function custom_quick_translate_title( $title ){
  $text_domain = defined( 'TEXT_DOMAIN' ) ? TEXT_DOMAIN : 'twentysixteen';

  return __( $title, $text_domain );
}
add_filter( 'the_title', 'custom_quick_translate_title' );

これで、投稿タイトルのテキストも翻訳候補になるので、extra-translate-text.phpに翻訳リスト定義してあげればOKだ。

このショートコードを使えば、投稿の一部だけとか、サイドバーだけとか、何気にフレキシブルに多国語化ができて便利かもしれない。