昨日は、AJAXのよいところを追加することで検索エンジンを終わらせました。
今日は、Jobeetの 国際化(Internationalization - i18n)と ローカライゼーション(Localization - l10n)を話します。
Wikipediaより:
国際化とはエンジニアリングの変更なしにさまざまな言語と地域に適応できるようにするソフトウェアアプリケーションの設計プロセスです。
ローカライゼーション とはロケール固有のコンポーネントを追加しテキストを翻訳することで特定の地域もしくは言語用にソフトウェアを適応させるプロセスです。
いつものように、symfonyフレームワークは車輪を再発明しないので国際化(i18n)とローカライゼーションはICU標準に基づいてサポートされます。
ユーザー
ユーザーのいない国際化はありえません。 世界の異なる地域もしくは異なる言語でWebサイトが利用できるとき、ユーザーは自身にもっともフィットする言語を選ぶことができます。
note
すでにsymfonyのUserクラスは13日目に話しました。
ユーザーculture
symfonyの国際化とローカライゼーションの機能はユーザーcultureに基づいています。
cultureはユーザーの言語と国の組み合わせです。
たとえば、フランス語を話すユーザーcultureはfr
でフランス出身のユーザーはfr_FR
です。
UserオブジェクトのsetCulture()
とgetCulture()
メソッドを呼び出すことでユーザーcultureを管理できます:
// アクションにおいて $this->getUser()->setCulture('fr_BE'); echo $this->getUser()->getCulture();
tip
ISO 639-1標準に従って、言語コードは小文字の2文字で構成されます。
オプションとしてのculture
デフォルトでは、ユーザーcultureはsettings.yml
設定ファイルで設定できます:
# apps/frontend/config/settings.yml all: .settings: default_culture: it_IT
tip
cultureはUserオブジェクトによって管理されるので、これはユーザーセッションに保存されます。 開発期間に、デフォルトcultureを変更する場合ブラウザーで新しい設定を有効にするためにセッションCookieをクリアしなければなりません
ユーザーがJobeetのWebサイトでセッションを始めるとき、HTTPヘッダーのAccept-Language
によって提供される情報に基づいてベストなcultureを決定することもできます。
リクエストオブジェクトのgetLanguages()
メソッドは現在のユーザーに許容される言語の配列を返します。
これはプリファレンスの順序によってソートされます:
// アクションにおいて $languages = $request->getLanguages();
しかしたいていの場合、あなたのWebサイトでは世界の136の主要な言語は利用できません。
ユーザーが選択した言語とWebサイトでサポートされる言語を比較することでgetPreferredCulture()
メソッドはベストな言語を返します:
// アクションにおいて $language = $request->getPreferredCulture(array('en', 'fr'));
以前の呼び出しで、ユーザーが選択した言語に従って返される言語は英語もしくはフランス語のどちらか何に一致しない場合は英語(配列の最初の言語)です。
URLのculture
JobeetのWebサイトは英語とフランス語の両方で利用できます。
URLは単独のリソースのみを表すので、cultureをURLに埋め込まなければなりません。
これを行うためには、routing.yml
ファイルを開き、api_jobs
とhomepage
以外のすべてのルートに対して特殊な変数:sf_culture
を追加します。シンプルなルートに関して、/:sf_culture
をurl
の前に追加します。
コレクションルートに関して、/:sf_culture
で始まるprefix_path
オプションを追加します。
# apps/frontend/config/routing.yml affiliate: class: sfDoctrineRouteCollection options: model: JobeetAffiliate actions: [new, create] object_actions: { wait: get } prefix_path: /:sf_culture/affiliate category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object } requirements: sf_format: (?:html|atom) job_search: url: /:sf_culture/search param: { module: job, action: search } job: class: sfDoctrineRouteCollection options: model: JobeetJob column: token object_actions: { publish: put, extend: put } prefix_path: /:sf_culture/job requirements: token: \w+ job_show_user: url: /:sf_culture/job/:company_slug/:location_slug/:id/:position_slug class: sfDoctrineRoute options: model: JobeetJob type: object method_for_query: retrieveActiveJob param: { module: job, action: show } requirements: id: \d+ sf_method: get
ルートでsf_culture
変数が使われるとき、ユーザーcultureを変更するためにsymfonyは自動的にこの値を使います。
多言語(/en/
、/fr/
、・・・)で多くのホームページをサポートする必要があるので、デフォルトのホームページ(/
)はユーザーcultureに従って適切にローカライズされたページにリダイレクトされなければなりません。
しかし、ユーザーがJobeetに初めて訪問し、ユーザーcultureを持たない場合、そのユーザーのためにcultureが選択されます。
最初に、isFirstRequest()
メソッドをmyUser
に追加します。
このメソッドはユーザーセッションの一番最初のリクエストに対してのみtrue
を返します:
// apps/frontend/lib/myUser.class.php public function isFirstRequest($boolean = null) { if (is_null($boolean)) { return $this->getAttribute('first_request', true); } else { $this->setAttribute('first_request', $boolean); } }
localized_homepage
ルートを追加します:
# apps/frontend/config/routing.yml localized_homepage: url: /:sf_culture/ param: { module: job, action: index } requirements: sf_culture: (?:fr|en)
セッションの最初のリクエストでユーザーを"ベストな"ホームページにリダイレクトするロジックを実装するためにjob
モジュールのindex
アクションを変更します:
// apps/frontend/modules/job/actions/actions.class.php public function executeIndex(sfWebRequest $request) { if (!$request->getParameter('sf_culture')) { if ($this->getUser()->isFirstRequest()) { $culture = $request->getPreferredCulture(array('en', 'fr')); $this->getUser()->setCulture($culture); $this->getUser()->isFirstRequest(false); } else { $culture = $this->getUser()->getCulture(); } $this->redirect('@localized_homepage'); } $this->categories = Doctrine::getTable('JobeetCategory')->getWithJobs(); }
リクエストの中にsf_culture
変数が存在しない場合、ユーザーは/
のURLに到達することを意味します。
これがあてはまりセッションが新しい場合、選択されたcultureがユーザーcultureとして使われます。
さもなければ現在のユーザーcultureが使われます。
最後のステップはユーザーをlocalized_homepage
のURLにリダイレクトすることです。
symfonyはsf_culture
変数を自動的に追加するのでsf_culture
変数はリダイレクト呼び出しに渡されていないことに注意してください。
これで、/it/
のURLに移動しようとすると、sf_culture
変数をen
もしくはfr
に制限したので、symfonyは404エラーを返します。
このルート要件をcultureを埋め込むすべてのルートに追加します:
requirements: sf_culture: (?:fr|en)
cultureのテスト
実装をテストしましょう。
しかしさらにテストを追加する前に、既存のものを修正する必要があります。
すべてのURLは変更されるので、test/functional/frontend/
の機能テストすべてを編集しすべてのURLの前に/en
を追加します。
lib/test/JobeetTestFunctional.class.php
ファイルのURLも変更することをお忘れなく。
テストを正しく修正したことを確認するためにテストスイートを立ち上げます:
$ php symfony test:functional frontend
ユーザーテスターは現在のユーザーのcultureをテストするisCulture()
メソッドを提供します。
jobActionsTest
ファイルを開き次のテストを追加します:
// test/functional/frontend/jobActionsTest.php $browser->setHttpHeader('ACCEPT_LANGUAGE', 'fr_FR,fr,en;q=0.7'); $browser-> info('6 - User culture')-> restart()-> info(' 6.1 - For the first request, symfony guesses the best culture')-> get('/')-> isRedirected()->followRedirect()-> with('user')->isCulture('fr')-> info(' 6.2 - Available cultures are en and fr')-> get('/it/')-> with('response')->isStatusCode(404) ; $browser->setHttpHeader('ACCEPT_LANGUAGE', 'en,fr;q=0.7'); $browser-> info(' 6.3 - The culture guessing is only for the first request')-> get('/')-> isRedirected()->followRedirect()-> with('user')->isCulture('fr') ;
言語の切り替え
cultureを変更するユーザーのために、言語フォームはレイアウトに追加しなければなりません。
フォームフレームワークはそのまま使えるフォームを提供しませんが多言語サイトの共通のニーズなので、symfonyコアチームはsfFormExtraPlugin
をメンテナンスしています。
このプラグインはバリデーター、ウィジェットとフォームを含みます。
これらはsymfonyのメインパッケージに含めることができません。
これらは限定的すぎるもしくは外部依存があるにせよとても役に立つものだからです。
plugin:install
タスクでこのプラグインをインストールします:
$ php symfony plugin:install sfFormExtraPlugin
プラグインは新しいクラスを定義するのでキャッシュをクリアします:
$ php symfony cc
note
sfFormExtraPlugin
はJavaScriptライブラリのような外部依存を必要とするウィジェットを含みます。
リッチな日付セレクター、WYSIWYGエディターなどもあります。
便利なものがたくさん見つかりましたらドキュメントを読むのに時間をかけてください。
sfFormExtraPlugin
プラグインは言語選択を管理するためにsfFormLanguage
フォームを提供します。
言語フォームは次のようにレイアウトに追加できます:
note
下記のコードは実装されることを意図していません。 これは間違ったやりかたで何かを実装したくなる例を示したものです。 symfonyを使って適切に実装する方法を示すことにとりかかります。
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php $form = new sfFormLanguage( $sf_user, array('languages' => array('en', 'fr')) ) ?> <form action="<?php echo url_for('@change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form> </div> </div>
問題を見つけられましたか? そうです、フォームオブジェクト作成はViewレイヤーに所属していません。 これはアクションから作成しなければなりません。 しかしこのコードはレイアウトにあるので、すべてのアクションに対してフォームが作成されます。 これは実用とはほど遠いものです。 このような場合、コンポーネント(component)を使います。 コンポーネントはパーシャルに似ていますがコードが添付されます。 軽量のアクションとみなしてください。
テンプレートからコンポーネントのインクルードするにはinclude_component()
ヘルパーを使うことで可能です:
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <!-- footer content --> <?php include_component('language', 'language') ?> </div> </div>
ヘルパーは引数としてモジュールとアクションを受け取ります。 3番目の引数はコンポーネントにパラメーターを渡すために使われます。
実際にユーザーの言語を変更するコンポーネントとアクションをホストするlanguage
モジュールを作成します:
$ php symfony generate:module frontend language
コンポーネントはactions/components.class.php
ファイルで定義されます。
では、ファイルを作りましょう:
// apps/frontend/modules/language/actions/components.class.php class languageComponents extends sfComponents { public function executeLanguage(sfWebRequest $request) { $this->form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); } }
ご覧のとおり、コンポーネントクラスはアクションクラスとよく似ています。
コンポーネント用のテンプレートはパーシャルと同じ命名規約を使います:
コンポーネントの名前の前にアンダースコア(_
)をつけます:
// apps/frontend/modules/language/templates/_language.php <form action="<?php echo url_for('@change_language') ?>"> <?php echo $form ?><input type="submit" value="ok" /> </form>
プラグインはユーザーcultureを変更するアクションを提供しないので、change_language
ルートを作成するためにrouting.yml
ファイルを編集します:
# apps/frontend/config/routing.yml change_language: url: /change_language param: { module: language, action: changeLanguage }
そして対応するアクションを作成します:
// apps/frontend/modules/language/actions/actions.class.php class languageActions extends sfActions { public function executeChangeLanguage(sfWebRequest $request) { $form = new sfFormLanguage( $this->getUser(), array('languages' => array('en', 'fr')) ); $form->process($request); return $this->redirect('@localized_homepage'); } }
sfFormLanguage
クラスのprocess()
メソッドは、ユーザーのフォーム投稿に基づいて、ユーザーcultureの変更を考慮します。
国際化
言語、文字集合、エンコーディング
異なる言語は異なる文字集合を持ちます。 英語はASCII文字のみを使うのでもっともシンプルですが、フランス語には"é"のようなアクセントつきの文字があるので少し複雑です。 ロシア語、中国語、アラビア語は文字がASCIIの外側にあるのではるかに複雑です。 このような言語は完全に異なる文字集合で定義されます。
国際化されたデータを扱うとき、Unicode標準を使うほうがよいです。 Unicodeの背景にあるアイディアはすべての言語のためのすべての文字を含む文字の全体集合です。 Unicodeの問題は単独の文字が21ビットで表されることです。 それゆえ、Webに関して、私たちはUTF-8を使います。 これはUnicodeコードポイントをオクテットの可変長のシーケンスにマッピングします。 UTF-8において、もっともよく使われる言語の文字は3ビット未満でコード化されます。
UTF-8はsymfonyで使われるデフォルトのエンコーディングで、settings.yml
設定ファイルで定義されます:
# apps/frontend/config/settings.yml all: .settings: charset: utf-8
また、symfonyの国際化レイヤーを有効にするには、settings.yml
でi18n
をon
にセットしなければなりません:
# apps/frontend/config/settings.yml all: .settings: i18n: on
テンプレート
国際化されたWebサイトとはユーザーインターフェイスが複数の言語に翻訳されることを意味します。
テンプレートにおいて、言語に依存する文字列は__()
ヘルパー(アンダースコアが2つあることに注意)でラップしなければなりません。
__()
ヘルパーはI18N
ヘルパーグループの一部です。
これはテンプレートの国際化管理を楽にしているヘルパーを含みます。
デフォルトではこのヘルパーグループはロードされないので、すでにText
ヘルパーグループに対して行ったようにuse_helper('I18N')
で手作業でそれぞれのテンプレートに追加するかstandard_helpers
設定を追加することでグローバルにロードする必要があります:
# apps/frontend/config/settings.yml all: .settings: standard_helpers: [Partial, Cache, I18N]
Jobeetのフッターに対する__()
ヘルパーの使い方です:
// apps/frontend/templates/layout.php <div id="footer"> <div class="content"> <span class="symfony"> <img src="/legacy/images/jobeet-mini.png" /> powered by <a href="/"> <img src="/legacy/images/symfony.gif" alt="symfony framework" /></a> </span> <ul> <li> <a href=""><?php echo __('About Jobeet') ?></a> </li> <li class="feed"> <?php echo link_to(__('Full feed'), '@job?sf_format=atom') ?> </li> <li> <a href=""><?php echo __('Jobeet API') ?></a> </li> <li class="last"> <?php echo link_to(__('Become an affiliate'), '@affiliate_new') ?> </li> </ul> <?php include_component('language', 'language') ?> </div> </div>
note
__()
ヘルパーはデフォルトの言語用の文字列を受け取るもしくはそれぞれの文字列に対してユニークな識別子を使うこともできます。
これは単なる好みの問題です。
Jobeetに関して、テンプレートが読みやすいように先の戦略を使います。
symfonyがテンプレートをレンダリングするとき、__()
ヘルパーが呼び出されるたびに、symfonyは現在のユーザーのculture用の翻訳を探します。
翻訳が見つかればそれが使われ、そうでなければ、最初の引数とフォールバックの値として返されます。
すべての翻訳はカタログに保存されます。 国際化フレームワークは翻訳を保存するために多くの異なる戦略を提供します。 私たちは"XLIFF"フォーマットを使います。 これは標準で最も柔軟なものです。 これはadminジェネレーターとsymfonyのたいていのプラグインにも使われます。
note
保存する他のカタログはgettext
、MySQL
とSQLite
です。
いつものことですが、詳細はi18n APIをご覧ください。
i18n:extract
カタログファイルを手作業で作る代わりに、組み込みのi18n:extract
タスクを使います:
$ php symfony i18n:extract frontend fr --auto-save
i18n:extract
タスクはfrontend
アプリケーションでfr
に翻訳される必要のあるすべての文字列を見つけ対応するカタログを作成もしくは更新します。
--auto-save
オプションは新しい文字列をカタログに保存します。
もう存在しない文字列を自動的に削除するために--auto-delete
オプションを使うこともできます。
私たちの場合、作成したファイルを投入します:
<!-- apps/frontend/i18n/fr/messages.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"> <xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target/> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target/> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target/> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target/> </trans-unit> </body> </file> </xliff>
それぞれの翻訳は一意のid
属性を持つtrans-unit
タグで管理されます。
このファイルを編集してフランス語の翻訳を追加できます:
<!-- apps/frontend/i18n/fr/messages.xml --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE xliff PUBLIC "-//XLIFF//DTD XLIFF//EN" "http://www.oasis-open.org/committees/xliff/documents/xliff.dtd"> <xliff version="1.0"> <file source-language="EN" target-language="fr" datatype="plaintext" original="messages" date="2008-12-14T12:11:22Z" product-name="messages"> <header/> <body> <trans-unit id="1"> <source>About Jobeet</source> <target>A propos de Jobeet</target> </trans-unit> <trans-unit id="2"> <source>Feed</source> <target>Fil RSS</target> </trans-unit> <trans-unit id="3"> <source>Jobeet API</source> <target>API Jobeet</target> </trans-unit> <trans-unit id="4"> <source>Become an affiliate</source> <target>Devenir un affilié</target> </trans-unit> </body> </file> </xliff>
tip
XLIFFは標準フォーマットなので、翻訳作業を楽にしてくれるツールがたくさんあります。 Open Language ToolsはXLIFFエディターが統合されたオープンソースのJavaプロジェクトです。
tip
XLIFFはファイルをベースとしたフォーマットなので、symfonyの設定ファイル用に存在する同じ手続きとマージルールも適用できます。 I18nファイルは、プロジェクト、アプリケーションもしくはモジュール単位で存在可能で、多くの場合個別のファイルはよりグローバルな場所で見つかる翻訳を上書きできます。
引数で翻訳
国際化の背景にある主要な原則はセンテンス全体を翻訳することです。 しかし中には動的な値を埋め込むセンテンスがあります。 Jobeetにおいて、これは"more..."リンク用のホームページにあてはまります:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> and <?php echo link_to($count, 'category', $category) ?> more... </div>
求人の件数は変数で翻訳用のプレースホルダーによって置き換えられなければなりません:
<!-- apps/frontend/modules/job/templates/indexSuccess.php --> <div class="more_jobs"> <?php echo __('and %count% more...', array('%count%' => link_to($count, 'category', $category))) ?> </div>
翻訳される文字列は"and %count% more..."で%count%
プレースホルダーは__()
ヘルパーに2番目の引数として渡される値のおかげで、実行時に本当の数値に置き換えられます。
trans-unit
タグをmessages.xml
ファイルに挿入することで新しい文字列を手作業で追加するかファイルを自動的に更新するためにi18n:extract
タスクを使います:
$ php symfony i18n:extract frontend fr --auto-save
タスクを実行した後で、フランス語翻訳を追加するためにXLIFFファイルを開きます:
<trans-unit id="5"> <source>and %count% more...</source> <target>et %count% autres...</target> </trans-unit>
翻訳された文字列の唯一の要件はどこかで%count%
プレースホルダーを使うことです。
数、センテンスへの変化に応じた複数形を持つためにより複雑な文字列がありますが、 すべての言語で必ずしも同じ方法ではありません。 ポーランド語やロシア語のように、複数形に関して非常に複雑な文法ルールを持つ言語があります。
カテゴリページにおいて、現在のカテゴリの求人件数が表示されます:
<!-- apps/frontend/modules/category/templates/showSuccess.php --> <strong><?php echo $pager->getNbResults() ?></strong> jobs in this category
センテンスが件数に応じて異なる翻訳を持つとき、format_number_choice()
ヘルパーが使われます:
<?php echo format_number_choice( '[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category', array('%count%' => '<strong>'.$pager->getNbResults().'</strong>'), $pager->getNbResults() ) ?>
format_number_choice()
ヘルパーは3つの引数を受け取ります:
- 件数に応じて使う文字列
- プレースホルダーの置き換えの配列
- 使用するテキストを決定するために使う数
件数に応じて翻訳を記述する文字列は次のようにフォーマットされます:
- それぞれのpossibilityはパイプ文字(
|
)で隔てられる - それぞれの文字列は翻訳の後に続く範囲で構成される
範囲は範囲の数を記述できます:
[1,2]
: 境界値を含み、1と2の間の値を受け取る(1,2)
: 境界値は含まず、1と2の間の値を受け取る{1,2,3,4}
: 集合で定義される値のみが受け取られる[-Inf,0)
: 負の無限大より大きいもしくは等しく、 0よりも小さい値を受け取る{n: n % 10 > 1 && n % 10 < 5}
: 2, 3, 4, 22, 23, 24のような数にマッチする
文字列の翻訳は他のメッセージ文字列と似ています:
<trans-unit id="6"> <source>[0]No job in this category|[1]One job in this category|(1,+Inf]%count% jobs in this category</source> <target>[0]Aucune annonce dans cette catégorie|[1]Une annonce dans cette catégorie|(1,+Inf]%count% annonces dans cette catégorie</target> </trans-unit>
すべての種類の文字列を国際化する方法を理解したので、フロントエンドアプリケーションのすべてのテンプレートに対して__()
の呼び出しを追加してみましょう。
バックエンドアプリケーションは国際化しません。
フォーム
ラベル、エラーメッセージとヘルプメッセージのようにフォームクラスは翻訳される必要のある文字列をたくさん含んでいます。 これらすべての文字列はsymfonyによって自動的に国際化されるので、XLIFFファイルで翻訳を提供することだけが必要です。
note
不幸なことに、i18n:extract
タスクは未翻訳の文字列のためにフォームクラスを解析しません。
Doctrineオブジェクト
JobeetのWebサイトのために、すべてのテーブルを国際化しません。 求人投稿をすべて利用可能な言語に翻訳するかを求人の投稿者にたずねるのは意味がないからです。 しかしあきらかにカテゴリテーブルは翻訳する必要があります。
Doctrineプラグインはi18nテーブルをネイティブにサポートします。 ローカライズされたデータを格納するそれぞれのテーブルに対して、2つのテーブルを作成する必要があります: 1つは国際化から独立しているカラムを持ち、もう一方は国際化するために必要なカラムを持ちます。 2つのテーブルは一対多のリレーションでリンクされます。
schema.yml
|schema.yml
(I18n)を更新します:
# config/doctrine/schema.yml JobeetCategory: actAs: Timestampable: ~ I18n: fields: [name] actAs: Sluggable: { fields: [name], uniqueBy: [lang, name] } columns: name: { type: string(255), notnull: true }
I18n
ビヘイビアを有効にすることで、JobeetCategoryTranslation
という名前のモデルが自動的に作成され指定されたfields
がそのモデルに移動させられます。
I18n
ビヘイビアを有効にして自動的に作成されるJobeetCategoryTranslation
モデルに添付されるSluggable
ビヘイビアを移動させていることに注目してください。
uniqueBy
オプションはスラッグが一意的かそうではないかをフィールドが決定することをSluggable
ビヘイビアに伝えます。
この場合それぞれのスラッグはlang
とname
の組に対して一意的でなければなりません。
カテゴリ用のフィクスチャを更新します:
# data/fixtures/categories.yml JobeetCategory: design: Translation: en: name: Design fr: name: design programming: Translation: en: name: Programming fr: name: Programmation manager: Translation: en: name: Manager fr: name: Manager administrator: Translation: en: name: Administrator fr: name: Administrateur
JobeetCategoryTable
のfindOneBySlug()
メソッドをオーバーライドすることも必要です。
Doctrineはモデルのすべてのカラム用のマジックファインダーを提供するので、Doctrineが提供するデフォルトのマジック機能をオーバーライドするためにfindOneBySlug()
メソッドを作ることが必要です。
カテゴリがJobeetCategoryTranslation
テーブルの英語のスラッグに基づいて検索されるように少しの変更を行う必要があります。
// lib/model/doctrine/JobeetCategoryTable.cass.php public function findOneBySlug($slug) { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', 'en') ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); }
モデルをリビルドします:
$ php symfony doctrine:build-all-reload --no-confirmation $ php symfony cc
tip
doctrine:build-all-reload
はデータベースからすべてのテーブルとデータを削除するので、
guard:create-user
タスクでJobeetバックエンドにアクセスするユーザーを
再作成することを忘れないでください。
代わりに、自動的に追加するためにフィクスチャファイルを追加できます。
I18n
ビヘイビアを利用するとき、JobeetCategory
オブジェクトとJobeetCategoryTranslation
オブジェクトの間にプロキシが作成されるのでカテゴリの名前を検索するためのすべての古い関数はまだ動作し、現在のculture用の値を読み取ります。
$category = new JobeetCategory(); $category->setName('foo'); // 現在のculture用の名前を設定する $category->getName(); // 現在のculture用の名前を取得する $this->getUser()->setCulture('fr'); // アクションクラスから $category->setName('foo'); // フランス語の名前を設定する echo $category->getName(); // フランス語の名前を取得する
tip
データベースリクエストの回数を減らすために、クエリにJobeetCategoryTranslation
をJOINします。
これは1つのリクエストでメインオブジェクトとi18nオブジェクトを読み取ります。
$categories = Doctrine_Query::create() ->from('JobeetCategory c') ->leftJoin('c.Translation t WITH t.lang = ?', $culture) ->execute();
上記のWITH
キーワードはクエリのON
条件を自動的に追加する条件を追加します。
ですので、joinのON
条件は最終的に次のようになります。
LEFT JOIN c.Translation t ON c.id = t.id AND t.lang = ?
category
ルートはJobeetCategory
モデルクラスに結びつけられます。
slug
は
JobeetCategoryTranslation
の一部で、
ルートはCategory
オブジェクトを自動的に読み取ることができないからです。
ルーティングシステムを手助けするために、オブジェクトの読み取りを考慮するメソッドを作りましょう:
すでにfindOneBySlug()
をオーバーライドしたのでこれらのメソッドが共有されるようにもう少しリファクタリングしましょう。
findOneBySlugAndCulture()
メソッドを単に使うために新しいfindOneBySlugAndCulture()
とdoSelectForSlug()
メソッドを作り、findOneBySlug()
メソッドを変更します。
// lib/model/doctrine/JobeetCategoryTable.class.php public function doSelectForSlug($parameters) { return $this->findOneBySlugAndCulture($parameters['slug'], $parameters['sf_culture']); } public function findOneBySlugAndCulture($slug, $culture = 'en') { $q = $this->createQuery('a') ->leftJoin('a.Translation t') ->andWhere('t.lang = ?', $culture) ->andWhere('t.slug = ?', $slug); return $q->fetchOne(); } public function findOneBySlug($slug) { return $this->findOneBySlugAndCulture($slug, 'en'); } // ...
}
category
ルートにオブジェクトを読み取るdoSelectForSlug()
メソッドを使うよう指示するためにmethod
オプション|method
オプション(ルーティング)を使います:
# apps/frontend/config/routing.yml category: url: /:sf_culture/category/:slug.:sf_format class: sfDoctrineRoute param: { module: category, action: show, sf_format: html } options: { model: JobeetCategory, type: object, method: doSelectForSlug } requirements: sf_format: (?:html|atom)
カテゴリ用に適切なスラッグを再生成するためにフィクスチャをリロードする必要があります:
$ php symfony doctrine:data-load
これでcategory
ルートは国際化されカテゴリ用のURLは翻訳されたカテゴリスラッグを埋め込みます:
/frontend_dev.php/fr/category/programmation /frontend_dev.php/en/category/programming
adminジェネレーター
symfony 1.2.1のバグのため、edit
セクションのtitle
をコメントアウトする必要があります:
# apps/backend/modules/category/config/generator.yml edit: #title: Editing Category "%%name%%" (#%%id%%)
バックエンドに関して、英語とフランス語を同じフォームで編集できるようにすることを考えます:
embedI18N()
メソッドを使うことで国際化フォームを埋め込むことができます:
// lib/form/JobeetCategoryForm.class.php class JobeetCategoryForm extends BaseJobeetCategoryForm { public function configure() { unset( $this['jobeet_affiliates_list'], $this['created_at'], $this['updated_at'] ); $this->embedI18n(array('en', 'fr')); $this->widgetSchema->setLabel('en', 'English'); $this->widgetSchema->setLabel('fr', 'French'); } }
adminジェネレーターインターフェイスは国際化をネイティブでサポートします。
これには20以上の言語の翻訳が付属しており、新しい翻訳を追加もしくはカスタマイズするのはとても簡単です。
i18n
ディレクトリ(アプリケーションのlib/vendor/symfony/lib/plugins/sfDoctrinePlugin/i18n/
で見つかるadminの翻訳)
からカスタマイズしたい言語のファイルをコピーします。
アプリケーションのファイルはsymfonyのものにマージされるので、修正した文字列をアプリケーションファイルの中だけに留めておきます。
adminジェネレーターの翻訳ファイルは、fr/messages.xml
の代わりに、sf_admin.fr.xml
のように名づけられていることに注目してください。
実際のところ、messages
はsymfonyによって使われるデフォルトのカタログの名前で、アプリケーションの異なる部分の間のよりよい分離を可能にするために変更できます。
__()
ヘルパーを使うときにデフォルト以外のカタログを使う場合は指定する必要があります:
<?php echo __('About Jobeet', array(), 'jobeet') ?>
上記の__()
の呼び出しにおいて、symfonyはjobeet
カタログの"About Jobeet"の文字列を探します。
テスト
テストの修正は国際化への移行のための不可欠な部分です。
test/fixtures/categories.yml
で定義したフィクスチャファイルをコピーして
最初に、カテゴリ用のテストフィクスチャを更新します。
test
環境用のモデルをリビルドします:
$ php symfony doctrine:build-all-reload --no-confirmation --env=test
正しく動作しているかチェックするためにすべてのテストを立ち上げることができます:
$ php symfony test:all
note
Jobeet用のバックエンドインターフェイスを開発したとき、機能テストを書きませんでした。 しかしsymfonyコマンドラインでモジュールを作成するとき、symfonyはテストスタブも生成します。 これらのスタブを削除しても安全です。
ローカライゼーション
テンプレート
異なるcultureをサポートすることは日付と数値をフォーマットする異なる方法もサポートすることを意味します。 テンプレートにおいて、現在のユーザーcultureに基づいて、これらすべての違いを考慮することを手助けしてくれるいくつかのヘルパーを自由に使えます:
Date
ヘルパーグループ:
ヘルパー | 説明 |
---|---|
format_date() |
日付をフォーマットする |
format_datetime() |
日付をフォーマットする |
time_ago_in_words() |
単語で現在と指定した日付の間の経過時間を表示する |
distance_of_time_in_words() |
単語で2つの日付の間の経過時間を表示する |
format_daterange() |
日付の範囲をフォーマットする |
Number
ヘルパーグループ:
ヘルパー | 説明 |
---|---|
format_number() |
数値をフォーマットする |
format_currency() |
通貨をフォーマットする |
I18N
ヘルパーグループ
において:
ヘルパー | 説明 |
---|---|
format_country() |
国の名前を表示する |
format_language() |
言語の名前を表示する |
フォーム(I18n)
フォームフレームワークはローカライズされたデータ用のウィジェットとバリデーターをいくつか提供します:
sfWidgetFormI18nDate
sfWidgetFormI18nDateTime
sfWidgetFormI18nSelectCurrency
sfValidatorI18nChoiceLanguage
また明日
symfonyでは国際化とローカライゼーションは第一級の扱いを受けます。 symfonyはすべての基本的なツールを提供しコマンドラインタスクで素早く実行できるので、ユーザーにローカライズされたWebサイトを提供するのはとても簡単です。
明日はsymfonyプロジェクトを編成するためにたくさんのファイルを移動させ、異なるアプローチを探求するので、特別なチュートリアルに備えてください。
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.