Symfonyで複数言語に"いい感じに"対応したWebサイトを作る
メリークリスマス!Symfony Advent Calendar 2024 の25日目の記事です!🎄✨
Twitter (X) でもちょいちょいSymfonyネタを呟いてます。よろしければ フォロー お願いします🤲
はじめに
最近、お仕事で久しぶりに多言語対応のサービスを作る機会がありました。
Symfonyでは Translationコンポーネント を導入すれば基本的な多言語対応はとても簡単に実装できるわけですが、実は 色々いい感じに 対応しようとすると意外と考えることがたくさんあります。
そこでこの記事では、私が現時点で「まあこんな感じでやるのがええんちゃうか」と考えている方法についてご紹介できればと思います。
考慮漏れやより良い方法などについて見識をお持ちの方はお気軽にコメントいただけると嬉しいです🤲
なお、本稿ではオーソドックスに日本語と英語の2言語に対応するケースを例にとります。
0. Symfony Translationを導入する
$ composer require symfony/translation
framework:
default_locale: ja
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- en
providers:
symfony_advent_calendar: Symfonyアドベントカレンダー
symfony_advent_calendar: Symfony Advent Calendar
<p>{{ 'symfony_advent_calendar'|trans }}</p>
Symfonyアドベントカレンダー
1. URLにロケール名を含める
前項の時点では、default_locale
が ja
なので、ロケールを明示的に指定するプロセスがどこにもなければ、常に日本語の表記が出力される結果になります。
そこで、日本語を読みたいユーザーと英語を読みたいユーザーに対してそれぞれ明示的にロケールを設定する処理がどこかに必要になります。
例えば 悪い例として、Accept-Language
ヘッダーの内容をもとに暗黙的にロケールを設定するという方法も考えれます。
#[Route(path: '/', name: 'home_')]
class HomeController extends AbstractController
{
#[Route(path: '/', name: 'index', methods: ['GET'])]
public function index(Request $request): Response
{
$request->setLocale($request->getPreferredLanguage(['ja', 'en'])); // これ
return $this->render('home/index.html.twig');
}
}
しかし、これだと、同じURLでもユーザーのブラウザの言語設定によって出力される内容が変わってしまうのでURL正規化の原則に反しますし、検索エンジンのクローラーが一方の言語のページしかクロールしてくれないことになるためSEOにおいても非常に不利になってしまいます。
そのため、基本的にはURLにロケール名を含めるようにするのが大前提となります。
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
prefix: /{_locale}
requirements:
_locale: ja|en
_locale
はロケールをセットするための 特殊なルーティングパラメータ です。ja
または en
にしかマッチしないように設定しているので、
-
/ja/
にアクセスした場合:ロケールとしてja
がセットされた状態でHomeController::index()
にルートされる -
/en/
にアクセスした場合:ロケールとしてen
がセットされた状態でHomeController::index()
にルートされる -
/それ以外の文字列/
にアクセスした場合:404 Not Foundになる
という挙動になります。
2. ロケール共通のページしか提供しないURLもある場合
前項の対応では、すべてのルートについて /{_locale}
というprefixが適用されるため、言語に関係なく常に同一の内容を返したいベージがある場合でも、そのページのURLも ja
と en
の2パターン作られてしまいます。
例えば、Symfonyアプリケーションに Image
エンティティがあり、画像ファイルの実体はS3などの外部ストレージに置いてあって Image
エンティティはその参照だけを持っている、そして画像ファイルのパーマリンクとして /image/1
などのエンドポイントでS3上の画像を表示できるようにする、という仕様があるとしましょう。
この場合、ImageController::show(Image $image)
のようなルートでS3の署名付きURLを生成してリダイレクトするといった実装をすることになると思うのですが、このルートには多言語化は必要ないですよね。
このような場合には、routes.yaml
に以下のような設定を追記することで、特定のコントローラだけを多言語化の対象から除外することができます。
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
prefix: /{_locale}
requirements:
_locale: ja|en
+
+ localeless_controllers:
+ resource: '../src/Controller/{Image,SomethingOther}Controller.php'
+ type: attribute
3. ロケールを省略したURLへのアクセスをデフォルトロケールのURLにリダイレクトする
前項まででおおよそ対応完了なのですが、できれば以下のような感じで、ロケールを省略したURLへのアクセスはデフォルトロケールのURLにリダイレクトするようにしたいですよね。
-
/
→/ja/
-
/foo/bar
→/ja/foo/bar
これを実現するには、「リクエストされているURLにロケールを付加してリダイレクトするコントローラアクション」を作ってあげればよいです。
class AddLocaleController extends AbstractController
{
public function __invoke(Request $request): Response
{
$locale = $request->getDefaultLocale();
$url = $request->getRequestUri();
$url = "/{$locale}{$url}";
return $this->redirect($url, 301);
}
}
あるいは、常にデフォルトロケールにリダイレクトする代わりに、ここでは Accept-Language
ヘッダーの内容に応じて適切と思しきロケールにリダイレクトするようにしてもよいでしょう。
class AddLocaleController extends AbstractController
{
private const array LOCALES = ['ja', 'en'];
public function __invoke(Request $request): Response
{
$locale = $request->getPreferredLanguage(self::LOCALES);
$url = $request->getRequestUri();
$url = "/{$locale}{$url}";
return $this->redirect($url, 301);
}
}
その上で、ja
でも en
でもないパスから始まるURLへのリクエストはすべて一旦 AddLocaleController
に送り込む ようにすればよいです。
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
prefix: /{_locale}
requirements:
_locale: ja|en
localeless_controllers:
resource: '../src/Controller/{File,SomethingOther}Controller.php'
type: attribute
+
+ add_locale:
+ path: /{path}
+ requirements:
+ path: ^(?!ja|en).*
+ defaults:
+ _controller: App\Controller\AddLocaleController
まとめ
というわけで、
- まずはSymfony Translationを導入して
- 前提としてURLにロケール名を含めるようにして
- ロケール共通のページしか提供しないURLもある場合はルーティング設定で除外するようにして
- ロケールを省略したURLへのアクセスはデフォルトロケール(など)のURLにリダイレクトするようにすれば
いい感じに なると思います、というお話でした✋
今年も1年間、たくさんSymfony上でコードを書きました。来年も引き続きSymfonyへの感謝を胸に、OSSやコミュニティーへの貢献を頑張っていきたいと思います!
それでは皆さま、よいお年を!
Discussion