翻訳

「国際化」という言葉は、文字列あるいは他のロケール固有の単位を抽出して アプリケーションの外部に取り出し、ユーザのロケール (例えば言語や国) に基づいて 翻訳したり変換したりできるレイヤーに落とし込むことです。テキストにおいては、テキスト (あるいは「メッセージ」) をユーザーの言語に翻訳することのできる機能でラッピング することを意味します。

// テキストは *常に* 英語で表示されます
echo 'Hello World';

// テキストはエンドユーザーの言語に翻訳されるか、
// デフォルトの英語になります
echo $translator->trans('Hello World');

Note

ロケール という言葉は、大雑把に言うとユーザーの言語や国のことです。 アプリケーションが後で翻訳や他のフォーマットの違い (例えば通貨単位) を管理するのに使うことができます。 ISO639-1 言語 コード、アンダースコア (_) 、そして ISO3166 コード (例えば fr_FR がフランス語と国の フランスを意味します) を使うことをおすすめします。

この章では、アプリケーションが複数のロケールをサポートするための準備方法について学び、 その後、複数のロケールに対する翻訳の作成方法を学びます。全体として、このプロセスには いくつかの一般的な手順があります。

  1. Symfony の Translation コンポーネントを有効化し、設定します。
  2. Translator の呼び出しの中で文字列 (例えば「メッセージ」)をラッピングして 抽出します。
  3. アプリケーション内で各メッセージを翻訳するための翻訳リソースを、 サポートされるロケールごとに作成します。
  4. セッション内でユーザーのロケールを判断し、設定し、管理します。

設定

翻訳は、翻訳されたメッセージを探して返すためにユーザーのロケールを使用する、Translator サービス が担当します。

  • YAML
    # app/config/config.yml
    framework:
        translator: { fallback: en }
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:translator fallback="en" />
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'translator' => array('fallback' => 'en'),
    ));
    

fallback オプションは、ユーザーのロケール内に翻訳が含まれなかった時の フォールバックロケールを定義しています。

Tip

ロケールに対する翻訳が存在しない時、トランスレータはまず言語 (インスタンスに対するロケールが fr_FR の時は fr) を探します。 それでも失敗する場合、フォールバックロケールを使った訳を探します。

翻訳で使われるロケールは、ユーザセッション内に保存されているものです。

標準的な翻訳

テキストの翻訳は、translator サービス (Symfony\Component\Translation\Translator) を通じて行われます。テキストのブロック (メッセージ と呼びます) を翻訳するには、 :method:`Symfony\\Component\\Translation\\Translator::trans` メソッドを使用してください。 例として、コントローラの中から単純なメッセージを翻訳していると考えてください。:

public function indexAction()
{
    $t = $this->get('translator')->trans('Symfony2 is great');

    return new Response($t);
}

このコードが実行されると、 Symfony2 はユーザーの locale を基に “Symfony2 is great” というメッセージを翻訳しようとします。この動作のために、 ロケールごとに翻訳されたメッセージの集まりである「翻訳リソース」を通じて どのようにメッセージを翻訳するのかを Symfony2 に教える必要があります。翻訳の 「辞書」は幾つかの異なるフォーマットで作成することができます。 XLIFF が推奨される フォーマットです。

  • XML
    <!-- messages.fr.xliff -->
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Symfony2 is great</source>
                    <target>J'aime Symfony2</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • PHP
    // messages.fr.php
    return array(
        'Symfony2 is great' => 'J\'aime Symfony2',
    );
    
  • YAML
    # messages.fr.yml
    Symfony2 is great: J'aime Symfony2
    

ユーザーのロケールがフランス語 (例えば fr_FR または fr_BE) の時には、 メッセージは J'aime Symfony2 に翻訳されます。

翻訳のプロセス

実際にメッセージを翻訳するには、 Symfony2 はシンプルなプロセスで行います。

  • セッションに保存されているユーザーの locale を見つけ出します。
  • 翻訳済みメッセージのカタログが ロケール (例えば fr_FR) に定義されている 翻訳リソースからロードされます。フォールバックロケールからのメッセージも 同じようにロードされ、まだ存在していない場合にはカタログに追加されます。最終的な 結果は、翻訳の大きな「辞書」になります。詳しくは メッセージのカタログ を参照してください。
  • メッセージがカタログの中にある場合、翻訳結果が戻り値になります。カタログの中にない場合、 トランスレータは元のメッセージを返します。

trans() メソッドを使用する時は、 Symfony2 は適切なメッセージカタログの中から 一致する文字列を探し、その文字列を返します (メッセージが存在する場合)。

メッセージプレースホルダー

時によって、メッセージは翻訳の必要がある変数を含んでいることがあります。

public function indexAction($name)
{
    $t = $this->get('translator')->trans('Hello '.$name);

    return new Response($t);
}

ところが、トランスレータは変数部分 (例えば “Hello Ryan” や “Hello Fabien”) を含んだ完全に一致するメッセージを探そうとするので、このような文字列に対する 翻訳を行うのは無理です。 $name 変数の考えうるすべてのイテレーションに対して 訳をつける代わりに、変数を「プレースホルダー」で置き換えることができます。

public function indexAction($name)
{
    $t = $this->get('translator')->trans('Hello %name%', array('%name%' => $name));

    new Response($t);
}

これで、 Symfony2 はそのままのメッセージ (Hello %name%) の翻訳を探すようになります。 そして その後で プレースホルダーを変数の値に置き換えます。翻訳の生成は前と同じように行われます。

  • XML
    <!-- messages.fr.xliff -->
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Hello %name%</source>
                    <target>Bonjour %name%</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • PHP
    // messages.fr.php
    return array(
        'Hello %name%' => 'Bonjour %name%',
    );
    
  • YAML
    # messages.fr.yml
    'Hello %name%': Hello %name%
    

Note

全体のメッセージが PHP の strtr 関数 で再構築されるように、プレースホルダーは どのようなかたちをとることもできます。しかし、 Twig テンプレート内で翻訳を行う時は、 %var% 表記が必須になるので、全体として従うに値する規約といえます。

ここまで見てきたように、翻訳を作成するには2つのステップがあります。

  1. Translator を通じて処理を行うことによって、翻訳に必要なメッセージを 抽出します。
  2. サポートしたいロケールごとにメッセージの翻訳を作成します。

次のステップは異なるロケールに対する翻訳を定義したメッセージのカタログの作成です。

メッセージのカタログ

メッセージが翻訳された時、 Symfony2 はユーザーのロケールに対するメッセージの カタログをコンパイルし、メッセージの翻訳を探します。メッセージのカタログは、 特定のロケールに対する翻訳の辞書のようなものです。例えば、fr_FR ロケールに 対するカタログは、以下のような訳を含んでいます。

Symfony2 is Great => J’aime Symfony2

これらの訳を作るのは、国際化されたアプリケーションの開発者 (または翻訳者) の責任です。翻訳はファイルシステム上に保存され、いくつかの規約の結果、 Symfony に発見されます。

Tip

新しく翻訳リソースを作成(または翻訳リソースを含んだバンドルをインストール)するたび、 Symfonyが新しい翻訳リソースを検出できるようキャッシュをクリアする必要があります。

php app/console cache:clear

翻訳の場所と名前付け規約

Symfony2 はメッセージファイル (例として翻訳) を2つの場所から探します。

  • バンドル内で見つけたメッセージに対しては、対応するメッセージファイルは バンドルの Resources/translations/ ディレクトリに存在する必要があります。
  • バンドルの翻訳をオーバーライドするには、メッセージファイルを app/Resources/translations に置いてください。

Symfony2 が翻訳の詳細を理解するのに規約を使用するので、翻訳のファイルネームも重要です。 それぞれのメッセージファイルは、ドメイン.ロケール.ローダー というパターンに沿って いなければなりません。

  • ドメイン: メッセージをグループに体系づける任意の方法です (例えば adminnavigation またはデフォルトの messages)。詳しくは メッセージドメインの使用 を参照してください。
  • ロケール: その翻訳のロケールです (例えば en_GBen など)。
  • ローダー: Symfony2 がどのようにファイルをロードし、パースするかです (例えば xliffphpyml)。

ローダーは、あらゆる登録済みのローダーの名前になり得ます。デフォルトでは、 Symfony は以下のローダーを提供しています。

  • xliff: XLIFF ファイル
  • php: PHP ファイル
  • yml: YAML ファイル

どのローダーを使用するかは完全にあなた (開発者) 次第で、好みの問題です。

Note

翻訳はデータベースや、 Symfony\Component\Translation\Loader\LoaderInterface の実装であるカスタムクラスによって定義されるその他のストレージに保存することも できます。どのようにカスタムローダーを登録するかは Custom Translation Loaders を参照してください (訳注 : 2011/03/13現在、この項は存在していない模様)。

翻訳の作成

それぞれのファイルは、与えられたドメインとロケールに対する ID と翻訳のペアの連なりから できています。この ID はそれぞれの翻訳の識別子になっており、アプリケーションあるいは ユニークな識別子 (例えば “symfony2.great” といったものです。詳しくはこの後の補足を 参照してください) のメインロケールのメッセージ(例えば “Symfony is great”)を引くことができます。

  • XML
    <!-- src/Acme/DemoBundle/Resources/translations/messages.fr.xliff -->
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="1">
                    <source>Symfony2 is great</source>
                    <target>J'aime Symfony2</target>
                </trans-unit>
                <trans-unit id="2">
                    <source>symfony2.great</source>
                    <target>J'aime Symfony2</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • PHP
    // src/Acme/DemoBundle/Resources/translations/messages.fr.php
    return array(
        'Symfony2 is great' => 'J\'aime Symfony2',
        'symfony2.great'    => 'J\'aime Symfony2',
    );
    
  • YAML
    # src/Acme/DemoBundle/Resources/translations/messages.fr.yml
    Symfony2 is great: J'aime Symfony2
    symfony2.great:    J'aime Symfony2
    

Symfony2 はこれらのファイルを見つけ出し、”Symfony2 is great” や “symfony2.great” の両方をフランス語ロケール (fr_FRfr_BE) に翻訳するのに使います。

メッセージドメインの使用

これまで見てきたように、メッセージファイルは翻訳されたロケールごとにまとめられます。 また、さらに「ドメイン」ごとにもまとめることができます。メッセージファイルを作成 するさい、ドメインはファイル名の最初の部分になります。デフォルトのドメインは messages です。例えば、管理上、翻訳が messagesadminnavigation という 3 つのドメインに分けられていると考えてください。フランス語の翻訳は以下の メッセージファイルになります。

  • messages.fr.xliff
  • admin.fr.xliff
  • navigation.fr.xliff

デフォルトドメイン (messages) 内に翻訳文字列がない時には、 trans() の 3 番目の引数としてドメイン名を指定する必要があります。

$this->get('translator')->trans('Symfony2 is great', array(), 'admin');

Symfony2 はここでユーザーのロケールの admin ドメイン内のメッセージを探します。

ユーザーロケールの扱い

現在のユーザのロケールはセッションに保存され、session サービスを介して アクセスできます。

$locale = $this->get('session')->getLocale();

$this->get('session')->setLocale('en_US');

フォールバックロケールとデフォルトロケール

セッション内でロケールが明確に指定されていない場合、fallback_locale 設定パラメータが Translator で使用されます。このパラメータのデフォルトは en です (詳しくは 設定 を参照してください) 。

もう一つの方法として、セッションサービスに default_locale を定義することで、 ユーザーのセッションにロケールが設定されていることを保証できます。

  • YAML
    # app/config/config.yml
    framework:
        session: { default_locale: en }
    
  • XML
    <!-- app/config/config.xml -->
    <framework:config>
        <framework:session default-locale="en" />
    </framework:config>
    
  • PHP
    // app/config/config.php
    $container->loadFromExtension('framework', array(
        'session' => array('default_locale' => 'en'),
    ));
    

ロケールと URL

ユーザーのロケールはセッション内に保存されるので、ユーザーのロケールに基づいた 色々な言語のリソースを表示するのに、同じ URL が使われることになります。 例えば、http://www.example.com/contact はあるユーザーには英語で、別なユーザーには フランス語で表示されます。残念ながら、これは Web の基本的なルール、すなわち、 ある URL はユーザーに関係なく同じリソースを返す、というルールに反しています。 さらに問題がややこしくなるのが、どのバージョンのコンテンツが検索エンジンでインデックス されるのか?ということです。

望ましいやり方は、URL にロケールを含めることです。これは、特別な _locale パラメータを使ったルーティングシステムで、完全にサポートされています。

  • YAML
    contact:
        pattern:   /{_locale}/contact
        defaults:  { _controller: AcmeDemoBundle:Contact:index, _locale: en }
        requirements:
            _locale: en|fr|de
    
  • XML
    <route id="contact" pattern="/{_locale}/contact">
        <default key="_controller">AcmeDemoBundle:Contact:index</default>
        <default key="_locale">en</default>
        <requirement key="_locale">en|fr|de</requirement>
    </route>
    
  • PHP
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('contact', new Route('/{_locale}/contact', array(
        '_controller' => 'AcmeDemoBundle:Contact:index',
        '_locale'     => 'en',
    ), array(
        '_locale'     => 'en|fr|de'
    )));
    
    return $collection;
    

ルートの中で特別な _locale パラメータを使用する際、一致するロケールが 自動的にユーザーセッションに設定されます 。言い換えると、ユーザーが /fr/contact という URI を訪れると、fr というロケールが自動的に ユーザーのセッションのロケールとして設定されます。

これで、ユーザーのロケールをアプリケーション内の他の翻訳されたページへのルートを 作るのに使用できるようになります。

複数型への対応

メッセージの複数型への対応は、ルールがなかなか複雑であるため、大きな問題です。 例えば、これはロシア語の複数型の数学的表現です。

(($number % 10 == 1) && ($number % 100 != 11)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2);

見ての通り、ロシア語では、それぞれ 0、1 あるいは 2 のインデックスを与えられた 3 つの異なる 複数型の表現があります。それぞれの表現で複数形は異なりますので、翻訳も同様に異なります。

複数型への対応のために翻訳の表現が異なる時、それら全ての表現をパイプ (|) で 区切られた文字列として与えることができます。

'There is one apple|There are %count% apples'

複数型に対応したメッセージを翻訳するため、 :method:`Symfony\\Component\\Translation\\Translator::transChoice` メソッドを使用できます。

$t = $this->get('translator')->transChoice(
    'There is one apple|There are %count% apples',
    10,
    array('%count%' => 10)
);

2つ目の引数 (この例では 10) は記述されるオブジェクトの であり、 どの翻訳が使われるかを決めるのに使われ、%count% プレースホルダーに 投入されます。

与えられた数字に従い、トランスレータは適切な複数型の表現を選びます。 英語の場合、多くの単語はぴったり1つしかオブジェクトがない時には 単数形で、それ以外の数 (0, 2, 3...) の時には複数型になります。 従って、count1 の時には、トランスレータ―は最初の文字列 (There is one apple) を翻訳として使い、そうでない場合は There are %count% apples を使用します。

以下はフランス語の翻訳です。

'Il y a %count% pomme|Il y a %count% pommes'

文字列は同じように見えたとしても (パイプで区切られた2つの部分文字列からなっています)、 フランス語の表現は異なります。最初の表現 (複数形ではない) は count01 の時に使われます。従って、トランスレータは count0 または 1 の時には自動的に最初の文字列 (Il y a %count% pomme) を使います。

それぞれのロケールは独自の表現のセットを持っています。いくつかのロケールは、どの数字が どの複数形にマップされるかの複雑なルールがある、6つの異なる複数形の表現を持っています。 英語とフランス語のルールはかなりシンプルですが、ロシア語では、どの表現がどの文字列に 一致するのか知るためにヒントが欲しくなるでしょう。翻訳者を手助けするために、 オプションとしてそれぞれの文字列に対して「タグをつける」ことができます。

'one: There is one apple|some: There are %count% apples'

'none_or_one: Il y a %count% pomme|some: Il y a %count% pommes'

タグは翻訳者のためのヒントでしかありませんので、どの複数形の表現を使うか決める ロジックには影響しません。タグはコロン (:) で終わる説明を含む文字列になります。 また、タグが翻訳された元のメッセージと同じである必要はありません。

間隔を明示した複数型への対応

メッセージを複数形にする最も簡単な方法は、与えられた数を元にどの文字列を選ぶかの Symfony2 の内部ロジックを使うことです。場合によっては、もっと翻訳の動作を自由に 制御したかったり、違う訳が欲しい時があるでしょう (例えば 0 に関してや、負の数の時)。 このような場合、明示的な数の間隔を使用できます。

'{0} There is no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf] There are many apples'

間隔は ISO 31-11 規格に従っています。上の文字列は 4 つの異なる間隔を定義しています。 ちょうど 0、ちょうど 12 から 1920 以上、です。

明示的な数の表現と、標準の表現を混在することもできます。この場合、数が指定された 間隔と一致しない時には、明示的な表現が削除された後に標準の表現が有効になります。

'{0} There is no apples|[20,Inf] There are many apples|There is one apple|a_few: There are %count% apples'

例えば、1 つのリンゴの場合、標準の表現 There is one apple が使われます。 2から19 個のリンゴの場合、2番目の標準の表現である There are %count% apples が使われます。

Symfony\Component\Translation\Interval クラスで数の有限集合を表せます。

{1,2,3,4}

または 2 と他の数の間なら以下のようになります。

[1, +Inf[
]-1,2[

左側のデリミターは [ (含む) または ] (含まない) になります。 右側のデリミターは [ (含まない) または ] (含む) です。 数と合わせて、-Inf+Inf を無限を表すのに使用できます。

テンプレート内の翻訳

多くの場合、翻訳はテンプレート内で発生します。 Symfony2 は Twig と PHP テンプレートの 両方をネイティブでサポートします。

Twig テンプレート

Symfony2 はメッセージの翻訳に役立つよう特別な Twig タグ (transtransChoice) を提供します。

{% trans %}Hello %name%{% endtrans %}

{% transchoice count %}
    {0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}

transChoice タグは自動的に現在のコンテキストから %count% 変数を取り出し、 トランスレータに渡します。このメカニズムは %var% というパターンに従った プレースホルダーを使用した場合にのみ動作します。

Tip

もし文字列内に(``%``)を使用する必要がある場合は、%の数を倍にすることでエスケープできます。``{% trans %}Percent: %percent%%%{% endtrans %}``

メッセージドメインを指定して、追加の変数を渡すこともできます。

{% trans with {'%name%': 'Fabien'} from "app" %}Hello %name%{% endtrans %}

{% transchoice count with {'%name%': 'Fabien'} from "app" %}
    {0} There is no apples|{1} There is one apple|]1,Inf] There are %count% apples
{% endtranschoice %}

``trans``、``transchoice``フィルターは、*variable texts*や複雑な式を変換するために使用できます。

{{ message | trans }}

{{ message | transchoice(5) }}

{{ message | trans({'%name%': 'Fabien'}, "app") }}

{{ message | transchoice(5, {'%name%': 'Fabien'}, 'app') }}

Tip

翻訳タグやフィルターには同じ効果がありますが、ひとつだけはっきりと違う部分があります。フィルターを使用した場合のみ、変数の自動エスケープ出力が行われるという点です。 もし翻訳した変数をエスケープ したくない 場合は、翻訳フィルターの後にrawフィルターを適用しなければなりません。

{# text translated between tags is never escaped #}
{% trans %}
    <h3>foo</h3>
{% endtrans %}

{% set message = '<h3>foo</h3>' %}

{# a variable translated via a filter is escaped by default #}
{{ message | trans | raw }}

{# but static strings are never escaped #}
{{ '<h3>foo</h3>' | trans }}

PHP テンプレート

トランスレータサービスへは、translator ヘルパーを通じて PHP テンプレートからも アクセスできます。

<?php echo $view['translator']->trans('Symfony2 is great') ?>

<?php echo $view['translator']->transChoice(
    '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10)
) ?>

翻訳ロケールの強制

メッセージの翻訳の際、 Symfony2 はユーザーセッションからのロケール、あるいは 必要な場合は フォールバック ロケールを使用します。同様に、翻訳で使用する ロケールを手動で指定することもできます。

$this->get('translator')->trans(
    'Symfony2 is great',
    array(),
    'messages',
    'fr_FR',
);

$this->get('translator')->trans(
    '{0} There is no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
    10,
    array('%count%' => 10),
    'messages',
    'fr_FR',
);

データベースコンテンツの翻訳

データベースコンテンツの翻訳は Translatable Extension を通じて Doctrine によって扱われるべきです。詳しくは、ライブラリのドキュメントを参照してください。

まとめ

Symfony2 の Translation コンポーネントを使用すると、国際化されたアプリケーションを 作ることはもはや苦痛なプロセスではなく、いくつかの基本的なステップに要約する ことができます。

  • :method:`Symfony\\Component\\Translation\\Translator::trans` メソッドまたは :method:`Symfony\\Component\\Translation\\Translator::transChoice` メソッドの いずれかでラッピングすることによって、アプリケーション内のメッセージを抽象化します。
  • それぞれのメッセージを、翻訳メッセージファイルを作成することで複数のロケールに 翻訳します。 メッセージファイルの名前は指定された規約に則っており、Symfony2 は それぞれのファイルを見つけ出して処理を行います。
  • セッションに保存されているユーザーのロケールを管理します。