🎻

Symfonyで複数言語に"いい感じに"対応したWebサイトを作る

2024/12/25に公開

メリークリスマス!Symfony Advent Calendar 2024 の25日目の記事です!🎄✨

Twitter (X) でもちょいちょいSymfonyネタを呟いてます。よろしければ フォロー お願いします🤲

はじめに

最近、お仕事で久しぶりに多言語対応のサービスを作る機会がありました。

Symfonyでは Translationコンポーネント を導入すれば基本的な多言語対応はとても簡単に実装できるわけですが、実は 色々いい感じに 対応しようとすると意外と考えることがたくさんあります。

そこでこの記事では、私が現時点で「まあこんな感じでやるのがええんちゃうか」と考えている方法についてご紹介できればと思います。

考慮漏れやより良い方法などについて見識をお持ちの方はお気軽にコメントいただけると嬉しいです🤲

なお、本稿ではオーソドックスに日本語と英語の2言語に対応するケースを例にとります。

0. Symfony Translationを導入する

$ composer require symfony/translation
config/packages/translation.yaml
framework:
  default_locale: ja
  translator:
    default_path: '%kernel.project_dir%/translations'
    fallbacks:
      - en
    providers:
translations/messages.ja.yaml
symfony_advent_calendar: Symfonyアドベントカレンダー
translations/messages.en.yaml
symfony_advent_calendar: Symfony Advent Calendar
templates/home/index.html.twig
<p>{{ 'symfony_advent_calendar'|trans }}</p>
表示結果
Symfonyアドベントカレンダー

1. URLにロケール名を含める

前項の時点では、default_localeja なので、ロケールを明示的に指定するプロセスがどこにもなければ、常に日本語の表記が出力される結果になります。

そこで、日本語を読みたいユーザーと英語を読みたいユーザーに対してそれぞれ明示的にロケールを設定する処理がどこかに必要になります。

例えば 悪い例としてAccept-Language ヘッダーの内容をもとに暗黙的にロケールを設定するという方法も考えれます。

src/Controller/HomeController.php
#[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にロケール名を含めるようにするのが大前提となります。

config/routes.yaml
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も jaen の2パターン作られてしまいます。

例えば、Symfonyアプリケーションに Image エンティティがあり、画像ファイルの実体はS3などの外部ストレージに置いてあって Image エンティティはその参照だけを持っている、そして画像ファイルのパーマリンクとして /image/1 などのエンドポイントでS3上の画像を表示できるようにする、という仕様があるとしましょう。

この場合、ImageController::show(Image $image) のようなルートでS3の署名付きURLを生成してリダイレクトするといった実装をすることになると思うのですが、このルートには多言語化は必要ないですよね。

このような場合には、routes.yaml に以下のような設定を追記することで、特定のコントローラだけを多言語化の対象から除外することができます。

config/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にロケールを付加してリダイレクトするコントローラアクション」を作ってあげればよいです。

src/Controller/AddLocaleController.php
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 ヘッダーの内容に応じて適切と思しきロケールにリダイレクトするようにしてもよいでしょう。

src/Controller/AddLocaleController.php
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 に送り込む ようにすればよいです。

config/routes.yaml
  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やコミュニティーへの貢献を頑張っていきたいと思います!

それでは皆さま、よいお年を!

GitHubで編集を提案

Discussion