第8章 - 国際化とローカライゼーション

人気のある Web アプリケーションの多くはさまざまな言語で利用可能で、ときにはユーザーカルチャに基づいてカスタマイズされます。 symfony はこれらの機能の管理を楽にする組み込みのフレームワークを搭載しています (symfony book の「国際化とローカライゼーション」の章を参照)。

フォームフレームワークはユーザーのインターフェイス用の組み込みサポートを備えており国際化オブジェクトの管理を楽にする方法を提供します。

フォームの国際化

symfony のフォームはデフォルトで国際化が可能です。 翻訳ファイルを編集することでラベルヘルパーテキストエラーメッセージを翻訳できます。 これらは XLIFFgettext もしくは symfony がサポートするその他のフォーマットです。

リスト8-1は以前の章で開発した問い合わせフォームを示します。

リスト8-1 - 問い合わせフォーム

[php]
class ContactForm extends BaseForm
{
  public function configure()
  {
    $this->setWidgets(array(
      'name'  => new sfWidgetFormInputText(),    // デフォルトのラベルは "Name"
      'email' => new sfWidgetFormInputText(),    // デフォルトのラベルは "Email"
      'body'  => new sfWidgetFormTextarea(), // デフォルトのラベルは "Body"
    ));

    // email ウィジェットのラベルを変更する
    $this->widgetSchema->setLabel('email', 'Email address');
  }
}

リスト8-2で示されるようにラベルのフランス語訳を XLIFF で定義します。

リスト8-2 - XLIFF 翻訳ファイル

[xml]
// apps/frontend/i18n/messages.fr.xml
<?xml version="1.0" ?>
<xliff version="1.0">
  <file original="global" source-language="en" datatype="plaintext">
    <body>
      <trans-unit>
        <source>Name</source>
        <target>Nom</target>
      </trans-unit>
      <trans-unit>
        <source>Email address</source>
        <target>Adresse email</target>
      </trans-unit>
      <trans-unit>
        <source>Body</source>
        <target>Message</target>
      </trans-unit>
    </body>
  </file>
</xliff>

翻訳のために使うカタログを指定する

symfony の国際化フレームワークの機能を利用する場合 (http://www.symfony-project.org/book/1_2/13-I18n-and-L10n#chapter_13_sub_managing_dictionaries)、フォームを特定のカタログにバインドできます。 リスト8-3において、ContactForm フォームを contact_form カタログに関連づけします。 ですので、フォーム要素の翻訳は contact_form.fr.xml ファイルで捜索されます。

リスト8-3 - 翻訳カタログのカスタマイズ

[php]
class ContactForm extends BaseForm
{
  public function configure()
  {
    // ...

    $this->widgetSchema->getFormFormatter()->setTranslationCatalogue('contact_form');
  }
}

Note カタログを利用することでたとえばフォームごとに1つのファイルを使用する翻訳の編成がよりベターになります。

エラーメッセージの国際化

ときどき、エラーメッセージにはユーザーによって投稿された値が埋め込まれます(たとえば、 「メールアドレスのuser@domainは有効ではありません。」)。 これをフォームクラスの中でカスタマイズされたエラーメッセージを定義してユーザーが投稿した値を簡単に利用できることを2章ですでに見ました。 これらの参照は %parameter_name% のパターンに従います。

リスト8-4はこの原則を問い合わせフォームの name フィールドに適用する方法を示しています。

リスト8-4 - エラーメッセージの国際化

[php]
class ContactForm extends BaseForm
{
  public function configure()
  {
    // ...

    $this->validatorSchema['name'] = new sfValidatorEmail(
      array('min_length' => 2, 'max_length' => 45),
      array('min_length' => 'Name "%value%" must be at least %min_length% characters.',
            'max_length' => 'Name "%value%" must not exceed %max_length% characters.',
      ),
    );
  }
}

リスト8-5で示されるように XLIFF ファイルを編集することでこれらのエラーメッセージを翻訳できます。

リスト8-5 - エラーメッセージ用の XLIFF 翻訳ファイル

[xml]
<trans-unit>
  <source>Name "%value%" must be at least %min_length% characters</source>
  <target>Le nom "%value%" doit comporter un minimum de %min_length% caractères</target>
</trans-unit>
<trans-unit>
  <source>Name "%value%" must not exceed %max_length% characters</source>
  <target>Le nom "%value%" ne peut comporter plus de %max_length% caractères</target>
</trans-unit>

翻訳オブジェクトのカスタマイズ

symfony の国際化フレームワークなしで symfony のフォームフレームワークを使いたい場合、翻訳オブジェクトを提供する必要があります。

翻訳オブジェクトは PHP の単なる callable です。これは次の3つのものの1つです:

  • my_function のような関数の名前を表す文字列

  • array($anObject, 'oneOfItsMethodsName') のような、クラスのインスタンスとメソッドの1つの名前への参照を伴う配列

  • sfCallable インスタンス。このカプセルは一貫した方法で PHP の callable をカプセル化します。

Note PHP の callable は関数もしくはメソッドのインスタンスへの参照です。 これは is_callable() 関数に渡されるときに true を返す PHP 変数でもあります。

リスト8-6で示されるクラスによって提供される独自の国際化メカニズムをすでに持つプロジェクトをマイグレートしなければならない場合を考えてみましょう。

リスト8-6 - カスタムの国際化クラス

[php]
class myI18n
{
  static protected $default_culture = 'en';
  static protected $messages = array('fr' => array(
    'Name'    => 'Nom',
    'Email'   => 'Courrier électronique',
    'Subject' => 'Sujet',
    'Body'    => 'Message',
  )); 

  static public function translateText($text)
  {
    $culture = isset($_SESSION['culture']) ? $_SESSION['culture'] : self::$default_culture; 
    if (array_key_exists($culture, self::$messages)
        && array_key_exists($text, self::$messages[$culture]))
    {
      return self::$messages[$_SESSION['culture']][$text];
    }
    return $text;
  }
}

// クラスの使い方
$myI18n = new myI18n();

$_SESSION['culture'] = 'en';
echo $myI18n->translateText('Subject'); // => "Subject"を表示する

$_SESSION['culture'] = 'fr';
echo $myI18n->translateText('Subject'); // => "Sujet"を表示する

リスト8-7で示されるようにそれぞれのフォームはフォームの要素の国際化を管理する独自のcallableを定義できます。

リスト8-7 - フォームのための国際化メソッドをオーバーライドする

[php]
class ContactForm extends BaseForm
{
  public function configure()
  {
    // ...
    $this->widgetSchema->getFormFormatter()->setTranslationCallable(array(new myI18n(), 'translateText'));
  }
}

パラメータとして受容できる翻訳の callable

翻訳の callable は3つの引数を取ります:

  • 翻訳するテキスト;

  • オリジナルのテキストの範囲で置き換える引数の連想配列。よくあるのはこの章で以前見たように動的な引数を置き換えるため;

  • テキストを翻訳するときに使うカタログの名前

下記のコードは翻訳の callable を呼び出すために sfFormWidgetSchemaFormatter::translate() メソッドによって使われる呼び出しです:

[php]
return call_user_func(self::$translationCallable, $subject, $parameters, $catalogue);

self::$translationCallable は翻訳の callable への参照です。ですので、以前のコードは下記のものと同等です:

[php]
$myI18n->translateText($subject, $parameters, $catalogue);

下記のコードはこれらの追加の引数をサポートする MyI18n クラスのアップデートされたバージョンです:

[php]
class myI18n
{
  static protected $default_culture = 'en';
  static protected $messages = array('fr' => array(
    'messages' => array(
      'Name'    => 'Nom',
      'Email'   => 'Courrier électronique',
      'Subject' => 'Sujet',
      'Body'    => 'Message',
    ),
  ));

  static public function translateText($text, $arguments = array(), $catalogue = 'messages')
  {
    $culture = isset($_SESSION['culture']) ? $_SESSION['culture'] : self::$default_culture; 
    if (array_key_exists($culture, self::$messages) &&
        array_key_exists($messages, self::$messages[$culture] &&
        array_key_exists($text, self::$messages[$culture][$messages]))
    {   
      $text = self::$messages[$_SESSION['culture']][$messages][$text];
      $text = strtr($text, $arguments);
    }   
    return $text;
  }
}

SIDEBAR 翻訳処理をカスタマイズするのになぜ sfWidgetFormSchemaFormatter を使うのか?

2章で見てきたように、フォームフレームワークは MVC アーキテクチャに基づき sfWidgetFormSchemaFormatter クラスはビューレイヤーに所属します。 このクラスはすべてのテキストのレンダリングに関与するので、すべてのテキストの文字列をインターセプトしてこれらを即時に翻訳します。

Propel のオブジェクトの国際化

フォームフレームワークは国際化された Propel オブジェクト用の組み込みサポート機能を持ちます。 これが動作する方法を説明するために国際化されたモデルの例を見てみましょう:

propel:
  article:
    id:
    author:     varchar(255)
    created_at:
  article_i18n:
    title:      varchar(255)
    content:    longvarchar

次のコマンドで Propel のクラスと関連するフォームクラスを生成できます:

$ php symfony build:model
$ php symfony build:forms

これらのコマンドはsymfonyのプロジェクトディレクトリの中で次のようなファイルを生成します:

lib/
  form/
    ArticleForm.class.php
    ArticleI18nForm.class.php
    BaseFormPropel.class.php
  model/
    Article.php
    ArticlePeer.php
    ArticleI18n.php
    ArticleI18nPeer.php

リスト8-8は同じフォームでフランス語と英語の記事を編集できるようにするために ArticleForm を設定する方法を示しています。

リスト8-8 - 国際化された Propel オブジェクト用の国際化フォーム

[php]
class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $this->embedI18n(array('en', 'fr'));
  }
}

リスト8-9で示されるように次のコードを configure() メソッドに追加することでフォームの言語ラベルをカスタマイズすることもできます。

リスト8-9 - 言語ラベルのカスタマイズ

[php]
$this->widgetSchema->setLabel('en', 'English');
$this->widgetSchema->setLabel('fr', 'French');

図8-1 - 国際化対応の Propel フォーム

国際化対応の Propel フォーム

これでお終いです。フォームオブジェクトの save() メソッドを呼び出すとき、関連する Propel オブジェクトと国際化オブジェクトは自動的に保存されます。

SIDEBAR ユーザーカルチャをフォームに渡す方法は?

フォームを現在のユーザーカルチャにバインドしたい場合、フォームを作るときに追加の culture オプションを渡すことができます:

[php]
class articleActions extends sfActions
{
  public function executeCreate($request)
  {
    $this->form = new ArticleForm(null, array('culture' => $this->getUser()->getCulture()));

    if ($request->isMethod('post') && $this->form->bindAndSave($request->getParameter('article')))
    {
      $this->redirect('article/created');
    }
  }
}

ArticleForm クラスにおいて、options の配列から値を取得できます:

[php]
class ArticleForm extends BaseArticleForm
{
  public function configure()
  {
    $this->embedI18n(array($this->getCurrentCulture()));
  }

  public function getCurrentCulture()
  {
    return isset($this->options['culture']) ? $this->options['culture'] : 'en';
  }
}

ローカライズされたウィジェット

symfony のフォームフレームワークは国際化を「認識する」(aware) いくつかのウィジェットを搭載しています。 ユーザーカルチャに従ってウィジェットをローカライズするためにこれらを使うことができます。

日付セレクタ

日付をローカライズするために利用できるウィジェットは下記のとおりです:

  • sfWidgetFormI18nDate ウィジェットは日付用の入力を表示します(日にち、月、年):

    [php]
    $this->widgetSchema['published_on'] = new sfWidgetFormI18nDate(array('culture' => 'fr'));
    

    3つの異なる値を受け取る month_format オプションのおかげで、月の表示フォーマットも定義できます:

    • 月の名前を表示するための name(デフォルト)
    • 月の短縮名を表示するための short_name
    • 月の番号を表示するための number(1から12)
  • sfWidgetFormI18nTime ウィジェットは時間用の入力を表示します (時、分、と秒):

    [php]
    $this->widgetSchema['published_on'] = new sfWidgetFormI18nTime(array('culture' => 'fr'));
    
  • sfWidgetFormI18nDateTime ウィジェットは日付と時間用の入力を表示します:

    [php]
    $this->widgetSchema['published_on'] = new sfWidgetFormI18nDateTime(array('culture' => 'fr'));
    

国セレクタ

sfWidgetFormI18nChoiceCountry ウィジェットは国のリストで満たされたセレクトボックスを表示します。 国の名前は指定された言語に翻訳されます:

[php]
$this->widgetSchema['country'] = new sfWidgetFormI18nChoiceCountry(array('culture' => 'fr'));

countries オプションのおかげで、セレクトボックス内の国を制限することもできます:

[php]
$countries = array('fr', 'en', 'es', 'de', 'nl');
$this->widgetSchema['country'] = new sfWidgetFormI18nChoiceCountry(array('culture'   => 'fr',
                                                                         'countries' => $countries));

カルチャセレクタ

sfWidgetFormI18nChoiceLanguage ウィジェットは言語の一覧で満たされたセレクトボックスを表示します。 言語の名前は指定された言語に翻訳されます:

[php]
$this->widgetSchema['language'] = new sfWidgetFormI18nChoiceLanguage(array('culture' => 'fr'));

languages オプションのおかげで、セレクトボックスの言語を制限することもできます:

[php]
$languages = array('fr', 'en', 'es', 'de', 'nl');
$this->widgetSchema['language'] = new sfWidgetFormI18nChoiceLanguage(array('culture'   => 'fr',
                                                                           'languages' => $languages));

                                                                           ### タイムゾーンセレクタ

sfWidgetFormI18nChoiceTimezone ウィジェットはタイムゾーンで満たされたセレクトボックスを表示します。

[php]
$this->widgetSchema['timezone'] = new sfWidgetFormI18nChoiceTimzione();