-
Notifications
You must be signed in to change notification settings - Fork 0
06 Localization and Resources
Woof は、グローバルな Web アプリケーション開発に不可欠な多言語対応 (ローカライズ) を、ビジネスロジックを汚さずにエレガントに実現する仕組みを提供します。
この章では、BCP 47 に準拠したロケール解析と、それに基づくリソースファイルの自動フォールバック機構、および実際の View と設定ファイルを多言語化する実践的なサンプルについて解説します。
クライアントが要求する言語情報は、通常 HTTP の Accept-Language ヘッダーを通じて渡されます。Woof では、この言語タグをパースし、適切なフォールバック順序 (チェーン) を保持する Locale オブジェクトを内部で自動的に構築します。
Operator::getLocale() を呼び出してロケールを取得する際、システムは以下の順番で優先度を評価し、ロケールを決定 (フォールバック) します。
- ユーザーのロケール: ブラウザから HTTP ヘッダーとして送信された要求言語。
-
アプリケーションのロケール:
Configで設定されたシステムの既定ロケール。例えばconfig/app.json内に"locale": "ja_JP"のように定義すると、Operator が自動的にこれを解釈して適用します。 - システムのロケール: サーバー (OS) の環境変数などから取得できる最終的なフォールバックロケール。
ロケールは、言語コード (例: ja) と地域コード (例: JP) の組み合わせなどで表現され、上位の具体的な指定から下位の一般的な指定へと段階的に解決されます。
多言語化された HTML テンプレートやテキスト、設定用の JSON ファイルなどを読み込む際、Woof では LocalizedResources クラスを使用します。
LocalizedResources は、基盤となる Resources オブジェクトと、ターゲットとなる Locale を組み合わせてインスタンス化します。これを利用して $localized->get("filename.json") のようにファイルを要求した際、Woof は指定されたロケールに応じて自動的に最適なファイルを検索します。
例えば、指定したロケールが ja-JP で、既定のアプリケーションロケール (app.locale) が en に設定されている場合、システムは resources ディレクトリ内を以下の優先順位で自動探索します。
-
filename-ja_JP.json(特定の国・地域向けに最適化されたファイル) -
filename-ja.json(言語が一致するファイル) -
filename-en.json(アプリケーションの既定ロケールに一致するファイル) -
filename.json(ロケール指定のないデフォルトのファイル)
この仕組みのおかげで、Controller や View の中に「言語が日本語ならこの処理を行う」といった if 分岐を記述する必要が一切なくなります。開発者は、ルールに従った名前のファイルをディレクトリに配置するだけで多言語化対応を完了できます。
リソースファイルや設定ファイルは、セキュリティの観点からドキュメントルート (公開ディレクトリ) よりも上の階層 (プロジェクトルート) に配置することが推奨されます。この後の実践デモを含めた、Woof プロジェクトの標準的なディレクトリツリーのイメージは以下のようになります。
project-root/
├── config/
│ ├── .gitignore (※ *.json や *.ini をバージョン管理対象外にする)
│ ├── app.json (※ "locale": "ja_JP" などを定義)
│ ├── session.json
│ ├── cache.json
│ ├── logger.json
│ └── database.json (※ 必要に応じてユーザーが自由に追加可能)
├── resources/
│ ├── time.html (デフォルト / 英語版のテンプレート)
│ ├── time.json (デフォルト / 英語版の文言設定)
│ ├── time-ja.html (日本語版のテンプレート)
│ ├── time-ja.json (日本語版の文言設定)
│ └── ... (他言語ファイル)
├── storage/
│ └── .gitignore (※ 自身以外の生成ファイルをバージョン管理対象外にする)
└── htdocs/ (※ Web サーバーのドキュメントルート)
├── index.php (フロントコントローラー)
├── js/
├── css/
└── images/
現在時刻と Cookie による訪問履歴を表示するアプリケーションを多言語に対応させる具体的な実装例を示します。環境に依存しない静的な文言やフォーマット定義は、すべて resources 内の JSON ファイルに集約します。
resources ディレクトリ内に、日本語、フランス語、中国語 (繁体字)、そしてデフォルト (英語版) の HTML テンプレートを配置します。
日本語用テンプレート (resources/time-ja.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>現在の時刻</title>
</head>
<body>
<h1>現在の時刻</h1>
<p>{{greeting}}</p>
<p>現在時刻は {{time}} です。</p>
</body>
</html>フランス語用テンプレート (resources/time-fr.html)
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Heure actuelle</title>
</head>
<body>
<h1>Heure actuelle</h1>
<p>{{greeting}}</p>
<p>L'heure actuelle est {{time}}.</p>
</body>
</html>中国語 (繁体字) 用テンプレート (resources/time-zh_TW.html)
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>當前時間</title>
</head>
<body>
<h1>當前時間</h1>
<p>{{greeting}}</p>
<p>現在時間是 {{time}}。</p>
</body>
</html>デフォルト / 英語版テンプレート (resources/time.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Current Time</title>
</head>
<body>
<h1>Current Time</h1>
<p>{{greeting}}</p>
<p>Current time is {{time}}.</p>
</body>
</html>それぞれの言語に応じたメッセージと、日付の書式フォーマットを定義した JSON ファイルを配置します。これらは HTML と同様にリソースファイルとしてバージョン管理されます。言語が追加されても、これらのファイルを追加するだけで済みます。
日本語用設定 (resources/time-ja.json)
{
"last-visited": "前回のアクセスは {{date}} でした。",
"first-message": "はじめまして。",
"date-format": "Y/m/d H:i:s"
}フランス語用設定 (resources/time-fr.json)
{
"last-visited": "Votre dernière visite remonte au {{date}}.",
"first-message": "C'est votre première visite.",
"date-format": "d/m/Y H:i:s"
}中国語 (繁体字) 用設定 (resources/time-zh_TW.json)
{
"last-visited": "您上次訪問的時間是 {{date}}。",
"first-message": "這是您的首次訪問。",
"date-format": "Y/m/d H:i:s"
}デフォルト / 英語版設定 (resources/time.json)
{
"last-visited": "Your last visit was on {{date}}.",
"first-message": "This is your first visit.",
"date-format": "F j, Y, g:i a"
}Controller 側では、 Operator から取得した Locale を用いて、その場で LocalizedResources を構築して JSON ファイルを読み込みます。これにより、PHP コード側から特定の言語名 ("ja" など) による条件分岐が完全に消失します。
<?php
use Woof\Web\Controller;
use Woof\Web\WebEnvironment;
use Woof\Http\Request;
use Woof\Http\Response;
use Woof\Web\Operator;
use Woof\LocalizedResources;
use Binder\Template;
class TimeController implements Controller
{
public function handle(Request $request, WebEnvironment $env): Response
{
$operator = new Operator($request, $env);
// 1. クライアントのロケールオブジェクトを取得
$locale = $operator->getLocale();
// 2. Controller 側で LocalizedResources を構築し、最適な JSON 設定を自動取得
$localized = new LocalizedResources($env->getResources(), $locale);
$jsonStr = $localized->get("time.json");
$json = json_decode($jsonStr, true);
// 3. Cookie から前回の訪問時刻を取得
$lastVisited = $request->getCookie("last_visited");
// 4. JSON から取得した定義に基づいて、挨拶文を動的にフォーマット
if ($lastVisited) {
$formattedDate = date($json["date-format"], (int)$lastVisited);
$greeting = Template::read($json["last-visited"])->entry()
->set("date", $formattedDate)
->render();
} else {
$greeting = $json["first-message"];
}
// 5. 副作用の隔離: 現在時刻を取得し、JSON 指定の書式でフォーマット
$now = $env->now();
$formattedTime = date($json["date-format"], $now);
return $operator
->setCookie("last_visited", (string) $now)
// View には描画に必要な情報と、フォールバックに必要な Locale をそのまま渡す
->setView(new TimeView($locale, $formattedTime, $greeting))
->build();
}
}View クラス側でも同様に、受け取った Locale と Resources から LocalizedResources を生成して、適切な言語の HTML テンプレートを自動探索してレンダリングします。
<?php
use Woof\Web\View;
use Woof\Resources;
use Woof\Web\Context;
use Woof\Locale;
use Woof\LocalizedResources;
use Binder\Template;
class TimeView implements View
{
/** @var Locale */
private $locale;
/** @var string */
private $time;
/** @var string */
private $greeting;
public function __construct(Locale $locale, string $time, string $greeting)
{
$this->locale = $locale;
$this->time = $time;
$this->greeting = $greeting;
}
public function getContentType(): string
{
return "text/html; charset=UTF-8";
}
public function render(Resources $resources, Context $context): string
{
// View 内部で LocalizedResources を作成して HTML を読み込む
$localized = new LocalizedResources($resources, $this->locale);
$html = $localized->get("time.html");
$homeUrl = $context->formatHref("/");
$privacyUrl = $context->formatHref("/privacy");
$contactUrl = $context->formatHref("/contact");
// テンプレートに値をバインドしてレンダリング
return Template::readMarkup($html)->entry()
->set("greeting", $this->greeting)
->set("time", $this->time)
->set("home_url", $homeUrl)
->set("privacy_url", $privacyUrl)
->set("contact_url", $contactUrl)
->render();
}
}