Contents
テーマハンドブックによると、国際化とは「テーマを他の言語に簡単に翻訳できるように開発するプロセスです。」と定義されています。これに従い、WordPress.org のテーマディレクトリ・プラグインディレクトリに自身のプロダクトを掲載したいときには、基本的にすべてのテキストが翻訳可能である必要があります。
この意味での国際化対応を行う場合、重要なポイントは以下です。
- テキストドメインを適切に設定する
- すべてのテキストに翻訳関数 (
__()など) を適用する
ですが、これは狭い意味での国際化であり、ブロックエディターハンドブックでは国際化は「ソフトウェア(この場合はWordPress)に複数の言語サポートを提供するプロセス」であると言及されています。つまり、単純にテキストドメインを設定したり翻訳関数を使用するだけでは不十分な場合があります。
この記事では、自身がこれまで WordPress の開発に貢献してきた中での経験を踏まえ、広い意味での国際化において重要だと思うものや、見逃されやすいものを紹介したいと思います。
翻訳
文字列の連結
全てのテキストは翻訳可能であることは基本ですが、例えばテキストの一部が動的に変化する場合があります。
// ❌ Don't
const fieldName = getFieldName();
const errorMessage = __( 'There is invalid text in the ', 'my-plugin' ) + fieldName + __( 'field.', 'my-plugin' );
これは非常に悪い例です。なぜなら、文法の観点から、テキストの順序が固定されてしまうからです。言語によって、主語・動詞・目的語の並び順は変化するため、たとえテキストの一部が動的に変化するとしても、それに対応できる実装にする必要があります。
WordPress では、このような問題に対処するため、プレースホルダーを使ったアプローチが推奨されます。翻訳者へ動的な要素に関するコンテキストを提供するために、Translator コメントも追加するとよいでしょう。
✅ Do
const fieldName = getFieldName();
const errorMessage = sprintf(
// translators: %s: field name.
__( 'Invalid text in %s field.', 'my-plugin' ),
fieldName
);
このほか、文字列の連結が問題を引き起こした珍しい例として、過去に Gutenberg プロジェクトであった「パーセンテージ」です。
// ❌ Don't
function Test( percentage ) {
return <p>{ `${ percentage }%` }</p>;
}
驚くことに、ロケールによってはパーセント記号の位置が入れ替わったり、文字そのものが変化したりしするため、ここでも文字列の連結を避ける必要があります。翻訳文字列に意味のあるテキストが全くないため、 translators コメントもあった方がよいでしょう。
// ✅ Do
import { __, sprintf } from '@wordpress/i18n';
function Test( percentage ) {
return (
<p>
{ sprintf(
/* translators: %d: Percentage value. */
__( '%d%%', 'my-plugin' ),
percentage
) }
</p>
);
}
リンクの国際化
テキストの中に、外部リソースへのリンクが埋め込まれる事があります。外部リソースによっては、いくつかのロケールに翻訳され、URL が異なる場合があります。そのような可能性がある場合は、リンク自体も変更できるようにします。
<?php
// ❌ Don't
_e( 'Please refer to <a href="https://example.com/">this handbook page</a> for more information.', 'my-plugin' );
// ✅ Do
printf(
__( 'Please refer to <a href="%s">this handbook page</a> for more information.', 'my-plugin' ),
esc_url( __( 'https://example.com/', 'my-plugin' ) )
);
なお、この書き方は PHP であれば問題ありませんが、React の場合は HTML がエスケープされてしまいます。少し面倒ですが、createInterpolateElement を使用して、文字列に含まれるタグ名を React 要素に変換する必要があります。
import { __ } from '@wordpress/i18n';
import createInterpolateElement from '@wordpress/element';
function Test() {
return (
<p>
{ createInterpolateElement(
__(
'Please refer to <a>this handbook page</a> for more information.', 'my-plugin'
),
{
a: <a href={ __( 'https://example.com/', 'my-plugin' ) } />,
}
) }
</p>
);
}
センテンスの連結
二つのセンテンスを連結するために、間に半角スペースをハードコードしてしまう場合があります。
// ❌ Don't
import { __ } from '@wordpress/i18n';
function Test() {
return (
<p>
{ __( 'It is sunny today.', 'my-plugin' ) }{ ' ' }
<strong>{ __( 'Tomorrow will be rainy.', 'my-plugin' ) }</strong>
</p>
);
}
これは、英語では以下のような HTML がレンダリングされます (細かい部分は省略しています)。
<p>It is sunny today. <strong>Tomorrow will be rainy.</strong></p>
英語では、センテンスの間にスペースを入れるのでこれは正しいですが、日本語ではどうでしょうか。
<p>今日は晴れです。 <strong>明日は雨でしょう。</strong></p>
「今日は晴れです。」の後にスペースがあります。日本語では、センテンスの間にスペースを入れないので、少し違和感があります。
このようなときは、前述のように、PHP の場合はHTML タグも含めて一つの翻訳文字列にします。React の場合は、createInterpolateElement を使用します。
// ✅ Do
import { __ } from '@wordpress/i18n';
import createInterpolateElement from '@wordpress/element';
function Test() {
return (
<p>
{ createInterpolateElement(
__( 'It is sunny today. <strong>Tomorrow will be rainy.</strong>', 'my-plugin' ),
{ strong: <strong /> }
) }
</p>
);
}
品詞転換
「品詞転換」とは、単語の形は変わらずに品詞が変化する現象の事です。例えば、以下のような翻訳文を想像してみてください。
// ❌ Don't
<h2><?php _e( 'Post', 'my-plugin' ); ?></h2>
<button><?php _e( 'Post', 'my-plugin' ); ?></button >
<h2><?php _e( 'View', 'my-plugin' ); ?></h2>
<button><?php _e( 'View', 'my-plugin' ); ?></button >
英語の場合、そのテキストが使われるコンテキストによって、名詞か動詞かを判断できます。しかし、名詞と動詞では違う単語が使われ、単一の単語では両方をカバーできないロケールがあります。
例えば日本語の場合、一例としては以下のようなテキストにしたいはずです。
<h2>投稿</h2>
<button>投稿する</button >
<h2>ビュー</h2>
<button>見る</button >
これを実現するようにするためには、翻訳文字列にコンテキストを提供することです。
// ✅ Do
<h2><?php _ex( 'Post', 'noun', 'my-plugin' ); ?></h2>
<button><?php _ex( 'Post', 'verb', 'my-plugin' ); ?></button>
<h2><?php _ex( 'View', 'noun', 'my-plugin' ); ?></h2>
<button><?php _ex( 'View', 'verb', 'my-plugin' ); ?></button>
このほか、実際に Gutenberg プロジェクトで起こった興味深い事例としては、「固有名詞と形容詞の品詞転換」です。
<button type="button"><?php _e( 'Small', 'my-plugin' ); ?></button >
<button type="button"><?php _e( 'Medium', 'my-plugin' ); ?></button >
<button type="button"><?php _e( 'Large', 'my-plugin' ); ?></button >
これは一見問題なさそうですが、「Medium」という Web サービスが存在しており、これは固有名詞です。
Gutenberg では、固有名詞としての「Medium」と、形容詞としての「Medium」を区別して翻訳できるように、固有名詞の方にコンテキストが追加されました。
// 固有名詞
<a><?php _ex( 'Medium', 'social link block variation name', 'my-plugin' ); ?></a>
// 形容詞
<button type="button"><?php _e( 'Medium', 'my-plugin' ); ?></button >
ブロック開発
save 関数では翻訳関数を使わない
ブロックを開発しているときに、テキストは変更可能であるものの、デフォルトのフォールバックテキストを設定しておきたい、かつそれをローカライズしたいという場合があります。
また、投稿本文にブロックコンテンツを保存したい場合、その情報を save 関数に定義する事が一般的です。例えば、save 関数を以下のように書いた場合はどうなるでしょうか。
// ❌ Don't
import { RichText, useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { content } = attributes;
return (
<div { ...useBlockProps.save() }>
<RichText.Content value={ content || __( 'Hello World', 'my-plugin' ) } />
</div>
);
}
このコードの意図は、ユーザーが設定するテキストを優先しつつ、ローカライズされたテキストをフォールバックとして用意しておきたい、というものです。このコードは一見問題なさそうに見えますが、この実装ではブロックが壊れる可能性があります。以下のようなフローを想定してみてください。
- ユーザーAは、WordPress のロケールを英語に設定している。
- ユーザーAは、このブロックを投稿に挿入し、保存する。
- ユーザーBは、WordPress のロケールを日本語に設定している。
- ユーザーBは、ユーザーAが投稿した投稿を開く。
ここでブロックが壊れます。
その理由は、ブロックのバリデーションによるものであり、投稿コンテンツに保存されている実際の HTML と、save 関数が生成する HTML が一致しているかどうかがチェックされ、一致しない場合はエラーを引き起こすからです。翻訳関数は、ユーザーのロケールに基づいて動的にテキストを変化させるため、投稿コンテンツに保存されているテキストと一致しない可能性があるということです。
これに関する理想的なアプローチはまだ見つかっていませんが、一つのアプローチは、ブロックを動的にして、フォールバックテキストをサーバーサイドでレンダリングするというものです。例えば「次のページ」ブロック (core/query-pagination-next) では、ブロックの attributes を優先しつつ、デフォルトテキストを翻訳可能にしています。
デフォルトテキストを block.json に定義しない
block.json において自動的に翻訳可能とみなされるテキストは、block-i18n.json ファイルに定義されたフィールドのみです。そのため、以下のように attributes のデフォルト値として文字列を定義しても、それが翻訳可能になることはありません。
{
"apiVersion": 3,
"name": "my-plugin/my-block",
"title": "My Block",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "div",
"default": "Hello World"
}
}
}
一つの解決策は、前のセクションで説明した通り、サーバーサイドでデフォルト値を設定することです。
block.json の example フィールドにコンテンツを定義しない
example フィールドは、主に ブロックのプレビューを表示するために使用されますが、前述の通り、このフィールドで定義される文字列は翻訳されません。
{
"apiVersion": 3,
"name": "my-plugin/my-block",
"title": "My Block",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "div",
"default": "Hello World"
}
},
"example": {
"attributes": {
"content": "Hello World"
}
}
}
解決策は、block.json ではなく、regisiterBlockType のプロパティに直接 example を定義する事です。
import { __ } from '@wordpress/i18n';
import { registerBlockType } from '@wordpress/blocks';
registerBlockType( 'my-plugin/my-block', {
apiVersion: 3,
title: __( 'My Block', 'my-plugin' ),
// ...
example: {
attributes: {
content: __( 'Hello World', 'my-plugin' ),
},
},
} );
RTL 言語
RTL (Right-to-Left) 言語とは、テキストが右から左に書かれる言語体系の事であり、代表的な言語としてアラビア語があります。一方英語と日本語は LTR (Left-to-Right) 言語です。WordPress は LTR 言語だけでなくRTL 言語もサポートしているので、 自身のプロダクトが RTL 言語でも正しく動作するかをチェックすることも重要な国際化対応の一つです。
スタイルシート
WordPress には、RTL 言語のためのスタイルシートを追加するための便利な仕組みがいくつか存在します。
その一つが、テーマのメインスタイルシートである style.css です。RTL 言語の時に完全に別のスタイルを読み込ませたいときは、style-rtl.css を用意して、そこに RTL 言語のためのスタイルを記述します。
もう一つの方法として、wp_style_add_data() 関数を使用して、任意のスタイルシートファイルを置換することが出来ます。
<?php
wp_enqueue_style( 'my-theme-style', get_template_directory_uri() . '/content.css', array(), wp_get_theme()->get( 'Version' ) );
// RTL styles.
wp_style_add_data( 'my-theme-style', 'rtl', 'replace' );
この例の場合、content.css ファイルと同じ階層に content-rtl.css を配置します。
ビルドツール
RTL 言語用のスタイルシートを自前で作成するのは大変です。なぜなら、物理プロパティすべてを反転させる必要があるからです。
/* LTR */
margin-left: 16px;
padding-right: 8px;
left: 0;
/* RTL */
margin-right: 16px;
padding-left: 8px;
right: 0;
最初から記述を論理プロパティで統一すればこのような処理は必要ありませんが、このような変換を自動化するために、WordPress コア、Gutenberg、デフォルトテーマでは、RTLCSS (もしくはそのラッパーライブラリ) が使用されています。
使い方は非常に簡単で、以下のように「元となる CSS ファイル」と「RTL 言語用の CSS ファイル」を指定するだけです。
rtlcss style.css style-rtl.css
ブロック開発の場合はより簡単です。テンプレート通りにブロックを開発している場合、@wordpress/scripts を使ってソースをビルドしていると思いますが、@wordpress/scripts は自動的に RTL 言語用の CSS ファイルも生成してくれます。またその CSS ファイルは、サイトのロケールに応じて自動的に読み込まれます。
RTLCSS を使う上で特に注意すべき点はありませんが、Control Directives については知っておく必要があります。もっとも使われるのは /*rtl:ignore*/ 構文で、これは物理プロパティの自動変換を禁止するためのものです。
.test {
/* rtl:ignore */
left: 10px;
}
アイコンの向き
RTL 言語のための最も基本的な対応は CSS によるものであり、RTLCSS などを使って適切なスタイルを提供するだけでも十分ですが、見落とされやすいものとして「画像・アイコンの方向」があります。
例えば、サイトエディターにある、シェブロンアイコンをもつリンクを見てみます。

RTL 言語のために物理プロパティを反転した場合、以下のようにレイアウトされます。

このアイコンの向きは正しくありません。意味的には右を向くはずです。
CSSを使って、RTL 言語の時だけアイコンを180度回転させることもできますが、Gutenberg でよくつかわれているアプローチは、isRTL() 関数でロケールを判別し、正反対のアイコンを読み込むことです。
import { __, isRTL } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { chevronLeft, chevronRight } from '@wordpress/icons';
function BackButton() {
return (
<Button icon={ isRTL() ? chevronRight : chevronLeft }>
{ __( 'Back', 'my-plugin' ) }
</Button>
);
}
フォーム要素
RTL 言語であるにもかかわらず、一部のコンテキストでは LTR を強制すべき場合があります。その一例として、email と url フィールドです。
これらのフィールドでは、基本的にラテン文字のみが入力されることが期待されるため、direction: ltr を適用します。前述の RTLCSS を使用している場合は、このプロパティの自動変換を禁止するために、ディレクティブを使用します。
input[type="email"],
input[type="url"] {
/* rtl:ignore */
direction: ltr;
}
この他、textarea 要素に関しては、コンテキストによって LTR を強制すべきかが変わってきます。例えば Gutenberg では、コードや HTML の入力が期待される textarea 要素では、LTR が強制されています。
非ラテン文字
非ラテン文字とは、アルファベット(A-Z)以外の文字体系の事で、日本語も非ラテン文字です。特に、ラテン文字である事を前提としたロジックで問題が起きやすいです。
文字のデコード
文字のデコードを正しく行わないと、期待される表示にならない例を紹介します。例えば、以下のようなコードで、投稿スラッグを取得・表示するためのロジックを実装します。
// ❌ Don't
import { useSelect } from '@wordpress/data';
export default function useSlugForDisplay() {
const slug = useSelect(
( select ) => select( 'core/editor' ).getEditedPostSlug(),
[]
);
return slug;
}
このロジックは、投稿スラッグがアルファベットのみで構成されている場合は問題ありません。ただし、スラッグが「投稿」などの非ラテン文字であった場合はどうでしょうか。
上記のフックの場合、%e6%8a%95%e7%a8%bfというエンコードされた文字列を取得するため、これは表示用としては正しくありません。
これを解決するには、decodeURIComponent、もしくはそのラッパー関数である safeDecodeURIComponent を使用します。
// ✅ Do
import { useSelect } from '@wordpress/data';
import { safeDecodeURIComponent } from '@wordpress/url';
export default function useSlugForDisplay() {
const slug = useSelect(
( select ) => select( 'core/editor' ).getEditedPostSlug(),
[]
);
return safeDecodeURIComponent( slug );
}
ユーザー入力値に基づくスラッグの生成
ユーザーからの入力値をもとに内部的に何らかの値を生成するとき、ラテン文字のみを前提としてる場合、意図しない動作を引き起こす場合があります。
例えば Gutenberg では、スラッグやプリセットのための文字列を生成するために、change-case ライブラリを使用して、ユーザー入力値を paramCase 関数に通していることがあります。ですが、paramCase 関数は非ラテン文字を削除するため、値が空になる危険性があります。
console.log( paramCase( 'Hello World' ) );
// > 'hello-world'
console.log( paramCase( 'こんにちは世界' ) );
// > ''
この問題を解決するアプローチは様々ですが、過去に Gutenberg で行われてきたことを参考にすると、以下のようなアプローチが考えられます。
- そもそも非ラテン文字の入力を拒否する。
- ユーザー入力に頼らず、何らかのインデックス番号またはランダムキーを使用する。
- ユーザー入力から生成した値が空であった場合、フォールバック値を使用する。
- Creating a new color in multibyte character, the style in editor will be broken. · Issue #39210 · WordPress/gutenberg
- Site Editor: Limit template part slugs to Latin chars by Mamaduka · Pull Request #38695 · WordPress/gutenberg
- Fix: save custom template with non-latin slug by t-hamano · Pull Request #69732 · WordPress/gutenberg
レイアウト・デザイン
要素の幅の変化
テキストが翻訳されるということは、ロケールによってそのテキストを含む要素のサイズが変化したり、テキストの折り返しが発生するということです。
すべてのテキストをすべてのロケールでテストすることは非現実的ですが、オーバーフローや折り返しによるレイアウトの崩れを事前に防ぐために、問題が発生しそうな箇所で以下のようなアプローチを試みておくことが大事です。
- 狭いコンテナに、幅が変化する可能性のある要素を詰め込まない。
overflow-x: autoを適用して、オーバーフローを許容する。- フレックスレイアウトにして、オーバーフローした要素を折り返す。
- オーバーフローを防ぐために、
word-break:{break-all|break-word|auto-phrase}を適用する。 text-overflow: ellipsisを適用して、オーバーフローしたテキストの末尾を「…」にして隠す。ただし、視覚的にテキストを切り詰めることになるため、アクセシビリティの観点からは多様しないほうが良いかもしれません。
Gutenberg での実際の例を一つだけ挙げると、投稿公開パネルです。投稿を公開すると、サイドバーには横並びのボタンが表示されます。このレイアウトは、少なくとも英語と日本語では問題ありません。
英語

日本語

ですが、ドイツ語 (de_DE) ではこれらのボタンのテキストが長いため、オーバーフローを防ぐため、ボタンが折り返されます。

年月日の順序
年、月、日の並び順は、国によって変わります。
- YMD: 日本、中国、韓国など
- MDY: 主にアメリカ
- DMY イギリス、フランス、ドイツなど
そのため、例えば年月日の入力フィールドが分かれており、以下のように並び順をハードコードした場合、特定のロケールでは不自然に感じられる場合があります。この並び順を、ロケールに応じて変化させるにはどうすればよいでしょうか?
<label for="year"><?php _e( 'Year', 'my-plugin' ); ?></label>
<input type="number" name="year" id="year" />
<label for="month"><?php _e( 'Month', 'my-plugin' ); ?></label>
<select name="month" id="month"></select>
<label for="day"><?php _e( 'Day', 'my-plugin' ); ?></label>
<select name="day" id="day"></select>
WordPress コアでは、これららの個々のフォーム要素を翻訳文字列のプレースホルダーとして扱い、ロケールによって並び順を変更できるようにしています。
/* translators: 1: Month, 2: Day, 3: Year, 4: Hour, 5: Minute. */
printf( __( '%1$s %2$s, %3$s at %4$s:%5$s', 'my-plugin' ), $month, $day, $year, $hour, $minute );
一方 Gutenberg では日付を入力するために便利な DateTimePicker コンポーネントがあります。このコンポーネントは dateOrder prop を持っており、その値によって年月日の並び順が自動的に変化します。そのため、このコンポーネントを使う場合は、その引数自体を翻訳可能にしておくことでこの問題に対処できます。
import { DateTimePicker } from '@wordpress/components';
const MyDateTimePicker = ( date, onChange ) => {
return (
<DateTimePicker
currentDate={ date }
onChange={ onChange }
dateOrder={
/* translators: Order of day, month, and year. Available formats are 'dmy', 'mdy', and 'ymd'. */
_x( 'dmy', 'date order', 'my-plugin' )
}
/>
);
};

コメントを残す