こんにちは、Trade Agency編集長のKです。現役のシステムアーキテクト兼トレーダーとして、日々トレードシステムの開発と市場の波に向き合っています。
昨今のグローバルなWebアプリケーション開発において、多言語対応(i18n)は避けて通れない要件です。しかし、フロントエンドの見た目を多言語化するのは簡単でも、バックエンドの非同期処理(メール送信など)まで完璧に言語を同期させるのは至難の業です。
今回は、LaravelとNext.jsを用いたモダンな構成において、ユーザー体験(UX)を極限まで損なわない【完全な多言語化アーキテクチャ】を構築する実践的なノウハウを公開します。

「画面は英語なのに、届いた二段階認証(OTP)メールが日本語だった」なんて経験ありませんか?開発側からすると「非同期だから仕方ない」と言い訳したくなりますが、ユーザーにとっては最悪の体験です。これを徹底的に潰していきますよ。
多言語化における最大の罠「非同期ワーカーでのコンテキスト喪失」
通常、多言語対応はHTTPリクエストのヘッダー(Accept-Languageなど)をミドルウェアでキャッチし、システム言語を切り替えることで実現します。しかし、メール送信などの重い処理をキュー(Queue)に入れて非同期化すると、重大な問題が発生します。

フロントエンドとバックエンドの言語を同期させる
まずは入り口の対策です。Next.js側の言語スイッチャーコンポーネントで、URLのロケールを切り替えるだけでなく、バックエンド(DB)のユーザー情報も同時に更新する仕組みを作ります。
// LanguageSwitcher.tsx (抜粋)
const switchLocale = async (nextLocale: string) => {
// ログイン中のユーザーであれば、バックエンドの言語設定も更新する
if (user) {
try {
await api.put('/user/locale', { locale: nextLocale });
} catch (error) {
console.error('Failed to update locale:', error);
}
}
// フロントエンドの言語切り替え(URLの書き換え等)
startTransition(() => {
router.replace(pathname, { locale: nextLocale });
});
};
ログイン中のユーザーなら、この一手間を入れるだけで、次回以降どの端末からアクセスしても自分の好みの言語が維持されるようになります。UX向上の第一歩ですね。
Laravelの神機能「HasLocalePreference」の活用
バックエンド側では、Laravelが用意している強力なインターフェースを活用します。Userモデルに【HasLocalePreference】を実装し、preferredLocaleメソッドでDBのlocaleカラムを返すように設定します。
// app/Models/User.php
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Authenticatable implements HasLocalePreference
{
/**
* 通知を送信する際にLaravelが自動参照する言語設定
*/
public function preferredLocale(): ?string
{
return $this->locale; // DBに保存された 'ja' や 'en' を返す
}
}しかし、これだけでは完璧ではありません。新規登録時、ログイン前のOTP送信、あるいは未認証ゲストからのお問い合わせなど、【ユーザーレコードが存在しない、またはDBの言語と現在画面で見ている言語が異なる】ケースが存在します。
例えば、普段は日本語設定のユーザーが、出先のPCで英語UIからログインしようとした場合、DBの言語を正としてしまうと「画面は英語、OTPメールは日本語」というチグハグな状態になります。
ここでのベストプラクティスは、コントローラー内で現在の実行言語を取得し、通知インスタンスに【直接言語を注入】することです。
// app/Http/Controllers/AuthController.php (ログイン時のOTP送信例)
// 1. ミドルウェアが設定した「現在の画面の言語」を取得
$currentLocale = \Illuminate\Support\Facades\App::getLocale();
// 2. 通知インスタンスに locale() をチェーンして非同期キューへ投げる
$user->notify(
(new TwoFactorCode($code))->locale($currentLocale)
);- App::getLocale() で現在画面の言語をキャッチする
- ->locale() メソッドで言語を強制指定する
- キューのペイロードに言語指定がシリアライズされ、ワーカーが確実に対象言語でメールを生成する
APIレスポンスと例外メッセージの多言語化
最後に、APIが返すJSONレスポンスや、内部でスローする例外メッセージから【ハードコーディングされた文字列を完全に排除】します。
// lang/ja/messages.php
return [
'internal_server_error' => 'サーバーエラーが発生しました。時間をおいて再度お試しください。',
'throttle_lockout' => '失敗回数が上限に達しました。約:minutes分後に再度お試しください。',
];
// Controller内での呼び出し例
return response()->json([
'message' => __('messages.internal_server_error')
], 500);
// 動的な変数を渡す場合
throw ValidationException::withMessages([
'login_id' => [__('messages.throttle_lockout', ['minutes' => $minutes])],
]);
「ユーザーが見つかりません」といった文字列をコントローラーに直接書くのはアーキテクチャの匂い(Code Smell)です。RateLimiter(スロットリング)などの変数もプレースホルダー(:minutes)を活用して、必ず言語ファイルに切り出しましょう。
よくある質問(FAQ)
- QNext.js側で言語を切り替えたのに、メールがデフォルト言語で届いてしまいます。
- A
非同期キューを利用している場合、ワーカープロセス内でHTTPリクエストのヘッダー情報が失われるためです。UserモデルにHasLocalePreferenceを実装するか、通知送信時に->locale()メソッドで言語を明示的に渡す必要があります。
- Q新規登録のメール認証(VerifyEmail)を非同期化するにはどうすればよいですか?
- A
Laravel標準のVerifyEmailは同期処理です。UserモデルのsendEmailVerificationNotificationメソッドをオーバーライドし、ShouldQueueを実装したカスタム通知クラスを呼び出すように変更してください。
- Q言語ファイル(lang)の管理が多くて大変です。良い管理方法はありますか?
- A
機能ごと(auth.php, messages.php, emails.phpなど)にファイルを分割し、フロントエンド(next-intlなど)とキー構造をある程度統一しておくと、開発体験が圧倒的に向上します。
まとめ
グローバル対応を見据えたシステムアーキテクチャにおいて、考慮すべきポイントを整理します。
- フロントエンドの言語切り替え時は、バックエンドのDBも同期させる
- 認証済みユーザーの非同期通知は、HasLocalePreferenceに任せる
- 未認証や認証の入り口(OTP等)では、現在画面の言語を明示的に通知に注入する
- APIのレスポンスや例外メッセージからハードコーディングを完全に排除する
神は細部に宿ります。システムのエッジケースを徹底的に潰し、世界中のユーザーに「違和感のないUX」を提供できるアーキテクチャを目指していきましょう。

コメント