Laravelで多言語対応を始めるとき、翻訳ファイルを用意するだけでは、利用者が安心して使える状態になりません。
URL、日付、通貨、通知、検索結果、読み上げまで同じロケール方針でつなぐ必要があります。
この記事では、既存アプリへ段階的に導入できるよう、設計からテストまでを実装順に整理します。
i18n、L10n、ロケールの違い
最初に三つの用語を分けると、実装範囲を判断しやすくなります。
| 用語 | 意味 | Laravelで扱う例 |
|---|---|---|
| i18n(国際化) | 複数の言語や地域を扱えるよう、アプリの構造を整えること | 翻訳キー、ロケール切替、言語別ルート |
| L10n(地域化) | 特定の言語や地域に合わせて表示や内容を調整すること | 日本語訳、日本時間、円表示 |
| ロケール | 言語と、必要に応じて地域を表す識別子 | ja、en、en_GB |
翻訳に使う言語ロケールと、日付や通貨に使う地域付きロケールは、同じとは限りません。
たとえば英語の文言を使う利用者でも、米国と英国では日付や通貨の表示要件が異なります。
実装前に決めること
コードを書く前に、対応範囲を一枚の表にします。
- 対応する言語と地域
- 各言語版を識別するURL
- 初回訪問時のロケール判定方法
- 利用者が選んだ言語の保存方法
- 日付、タイムゾーン、数値、通貨、単位の表示規則
- メール、通知、PDF、画像代替テキスト、動画字幕の翻訳範囲
- 右から左へ書く言語(RTL)の対象
- 翻訳の追加、レビュー、公開、差し戻しの担当
この表がないと、画面は日本語でもメールは英語、URLは共通でも検索結果は言語別という不整合が起きやすくなります。
翻訳ファイルを設計する
現在の標準構成を確認する
現在のLaravelドキュメントでは、言語ファイルはプロジェクト直下のlangディレクトリで扱います。
新しいプロジェクトでディレクトリがない場合は、php artisan lang:publishで公開できます。
旧構成から更新したプロジェクトではresources/langを使っている場合があるため、既存コードとフレームワークのバージョンを確認してください。
lang/
├─ en/
│ ├─ messages.php
│ └─ validation.php
├─ ja/
│ ├─ messages.php
│ └─ validation.php
├─ en.json
└─ ja.json
短いキーと原文キーを使い分ける
| 方式 | 例 | 向いている場面 | 注意点 |
|---|---|---|---|
| 短いキー | __('messages.saved') |
画面や業務領域ごとに文言を管理するアプリ | キー名だけで用途が分かる命名規則が必要 |
| 原文キー | __('Sign in') |
原文を見ながら翻訳を追加したい画面 | 原文の変更がキーの変更になる |
方式を混在させる場合は、恒久的なUI文言は短いキー、移行中の文言はJSONというように境界を決めます。
// lang/ja/messages.php
return [
'saved' => ':nameを保存しました。',
];
__('messages.saved', ['name' => '設定']);
単語だけを組み合わせて文を作ると、語順や助詞を言語ごとに調整できません。
翻訳単位は、利用者に表示する一文として意味が通る粒度にします。
複数形は言語ごとの規則として扱う
個数によって文言が変わる言語では、trans_choiceで範囲ごとの表現を定義できます。
// lang/en/messages.php
return [
'items' => '{0} No items|{1} :count item|[2,*] :count items',
];
echo trans_choice('messages.items', $count, ['count' => $count]);
日本語と英語で同じ分岐を機械的に複製せず、各言語で自然な文になるかを確認します。
ロケールをURLとミドルウェアで決める
同じページの言語版を共有できるようにするなら、/ja/...や/en/...のようにURLへロケールを含める方法が分かりやすい設計です。
言語がURLから決まるため、ブックマーク、共有、キャッシュ、検索エンジンの処理も再現しやすくなります。
| 判定方法 | 役割 | 実装上の注意 |
|---|---|---|
| URL | 表示する言語版を確定する | 対応外の値を許可しない |
| 利用者設定またはセッション | 明示的な選択を保存する | 共有URLの言語と矛盾させない |
Accept-Language |
初回訪問時の候補を推定する | 利用者の選択を上書きしない |
// routes/web.php
Route::group([
'prefix' => '{locale}',
'where' => ['locale' => 'ja|en'],
'middleware' => [SetLocale::class],
], function () {
Route::get('/', [HomeController::class, 'index'])
->name('home');
});
// app/Http/Middleware/SetLocale.php
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
final class SetLocale
{
public function handle(Request $request, Closure $next)
{
$locale = (string) $request->route('locale');
abort_unless(in_array($locale, ['ja', 'en'], true), 404);
App::setLocale($locale);
return $next($request);
}
}
Accept-Languageを読む場合は、単純に先頭二文字を採用する実装を避けます。
地域タグと優先度を含む可能性があるため、対応ロケールとの照合方法を決め、初回の候補提示やリダイレクトだけに使います。
日付、時刻、数値、通貨を地域化する
文言の言語と表示地域を分けておくと、英語圏の地域差や利用者ごとのタイムゾーンを扱いやすくなります。
$languageLocale = app()->getLocale();
$formatLocale = match ($languageLocale) {
'ja' => 'ja_JP',
'en' => 'en_US',
default => 'en_US',
};
日付とタイムゾーン
保存時の基準を統一し、表示直前に利用者のタイムゾーンへ変換します。
$displayedAt = $order->created_at
->copy()
->timezone($user->timezone ?? 'UTC')
->locale(app()->getLocale())
->isoFormat('LLL');
予約や締切の入力画面では、入力値がどのタイムゾーンとして保存されるかを画面上で示します。
数値と通貨
$number = new \NumberFormatter($formatLocale, \NumberFormatter::DECIMAL);
$quantityLabel = $number->format($quantity);
$currency = new \NumberFormatter($formatLocale, \NumberFormatter::CURRENCY);
$priceLabel = $currency->formatCurrency($price, $currencyCode);
通貨コードはロケールだけから推測せず、商品、契約、利用者設定など、アプリの業務ルールから決めます。
距離や重量を換算する場合も、元の値と単位を保持し、表示時に変換すると検証しやすくなります。
フォーム、バリデーション、通知をそろえる
入力項目名まで翻訳する
バリデーション文言だけでなく、:attributeへ入る項目名も言語ファイルで定義します。
// lang/ja/validation.php
'attributes' => [
'email' => 'メールアドレス',
'password' => 'パスワード',
],
住所、郵便番号、電話番号は地域によって形式が異なるため、例示、入力補助、検証規則を一組として確認します。
言語切替でフォームを再表示するときは、入力途中の値とエラーの対応関係が失われないかもテストします。
通知は受信者のロケールで送る
キューで後から送る通知は、HTTPリクエスト時のロケールに依存させず、受信者の設定を使います。
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Authenticatable implements HasLocalePreference
{
public function preferredLocale(): string
{
return $this->locale;
}
}
一回だけ明示する場合は、通知オブジェクトのlocaleを指定できます。
$user->notify(
(new VerifyEmail())->locale('ja')
);
通知の保存、配信、読み上げまで含む設計は、Laravel NotificationsとBroadcastingの実装ガイドで補足しています。
言語切替をアクセシブルにする
langとdirを別々に設定する
langは読み上げに使う言語を、dirは文字とレイアウトの方向を示します。
@php
$locale = app()->getLocale();
$htmlLang = str_replace('_', '-', $locale);
$direction = in_array($locale, ['ar', 'he'], true) ? 'rtl' : 'ltr';
@endphp
<html lang="{{ $htmlLang }}" dir="{{ $direction }}">
Laravelの言語ディレクトリ名ではen_GBのようなアンダースコアを使う一方、HTMLのlangやhreflangではen-GBのようなハイフン表記に変換します。
日本語本文中に別言語の語句や引用がある場合は、その要素へlangを付けます。
<p>サービス名は <span lang="en">Example Cloud</span> です。</p>
言語切替はリンクとして意味が通る形にする
<nav aria-label="{{ __('messages.language_switcher') }}">
<ul>
<li>
<a href="{{ route('home', ['locale' => 'ja']) }}"
lang="ja" hreflang="ja"
@if(app()->getLocale() === 'ja') aria-current="page" @endif>
日本語
</a>
</li>
<li>
<a href="{{ route('home', ['locale' => 'en']) }}"
lang="en" hreflang="en"
@if(app()->getLocale() === 'en') aria-current="page" @endif>
English
</a>
</li>
</ul>
</nav>
- 国旗だけで言語を表さず、各言語の名称を文字で示す
- 現在の言語には
aria-current="page"を付ける - 可能なら、切替後も同じ内容の言語版へ移動する
- クライアント側で本文だけを差し替える場合は、変更を通知し、主見出しへフォーカスを移せるようにする
多言語UIのフォームやRTLまで確認する項目は、多言語サイトのアクセシビリティ実装ガイドでも詳しく整理しています。
エラー、画像、動画も同じ言語にする
サーバー側でエラー一覧を表示する場合は、一覧へフォーカスを移せるようにし、各リンクから該当項目へ移動できるようにします。
動的に追加するエラー通知へライブリージョンを使う場合は、同じメッセージを重複して読み上げないかを実機で確認します。
画像の代替テキストは単語を直訳するのではなく、その画像が各言語の画面で果たす目的を伝えます。
動画には言語別の字幕トラックを用意し、字幕の言語をsrclangで指定します。
RTLで崩れにくいレイアウトを作る
RTLは文章を右寄せするだけの対応ではありません。
margin-inline-startやpadding-inline-endなどの論理プロパティを使うtext-align: startで書字方向に合わせる- 戻る、進むなど方向を持つアイコンだけを必要に応じて反転する
- 電話番号、コード、数式など、LTRの断片が混在する画面を確認する
- 長い翻訳でボタン、表、ナビゲーションが欠けないかを確認する
方向指定と文言の長さは別の問題なので、RTLへの切替と翻訳後のレイアウトを分けて試験します。
多言語SEOをURL設計とそろえる
hreflangは、同じ内容の言語版や地域版の関係を検索エンジンへ伝えるために使います。
<link rel="alternate" hreflang="ja"
href="https://example.com/ja/products/1">
<link rel="alternate" hreflang="en"
href="https://example.com/en/products/1">
<link rel="alternate" hreflang="x-default"
href="https://example.com/language-selector">
- 各言語版から、自分自身とすべての対応版を相互に参照する
hrefには絶対URLを使うx-defaultには、未対応言語の利用者を案内する実際のフォールバック先を指定する- タイトル、メタディスクリプション、OG情報も各言語で意味が通る内容にする
- サイトマップで管理する場合も、言語版どうしの対応関係を同じ規則で出力する
言語別URLが存在しない状態でhreflangだけを追加しても、対応するページの関係は作れません。
テストを層に分ける
| テスト層 | 確認すること |
|---|---|
| Featureテスト | 対応URLでロケールが設定され、対応外ロケールが拒否されること |
| 翻訳テスト | 主要画面、バリデーション、通知に未定義キーや原文の露出がないこと |
| 書式テスト | タイムゾーン、日付、数値、通貨が想定した地域規則で表示されること |
| ブラウザテスト | lang、dir、言語切替、入力値保持、キーボード操作が正しいこと |
| SEO検査 | 言語別URL、相互のhreflang、メタ情報、サイトマップが一致すること |
public function test_locale_via_prefix(): void
{
$this->get('/ja')
->assertOk()
->assertSee('ようこそ');
$this->get('/en')
->assertOk()
->assertSee('Welcome');
$this->get('/xx')
->assertNotFound();
}
テストでは単一の見本文言だけでなく、言語切替後も同じ商品や記事を表示しているかを確認します。
運用で翻訳の欠落を防ぐ
翻訳ファイルをコードと同じリポジトリで管理する場合は、追加、変更、削除をレビュー対象にします。
- 用語集を作り、同じ概念の訳語をそろえる
- 未定義キーと未翻訳の文言をCIで検出する
- 長い文言、複数形、変数を含む文言を重点的にレビューする
- 翻訳の公開時期を機能の公開時期と合わせる
- アクセスの多い画面から段階的に導入し、エラーと問い合わせを監視する
翻訳管理ツールを使う場合も、キーの命名、レビュー権限、差し戻し方法はアプリ側の運用規則として残します。
公開前チェックリスト
- 翻訳対象と対象外が決まっている
- 対応ロケールの許可リストが一か所で管理されている
- 言語別URLが共有できる
- 利用者が選んだ言語を
Accept-Languageより優先している - 言語ロケールと地域付き書式ロケールを区別している
- 日付、通貨、単位、タイムゾーンを手書きで連結していない
- バリデーション、メール、通知、画像代替テキスト、字幕も翻訳されている
langとdirが表示内容と一致している- 言語切替をキーボードと読み上げ環境で操作できる
hreflangが各言語版から相互に参照されている- 未翻訳とレイアウト崩れを継続して検出できる
多言語化は、一度に全画面へ広げる必要はありません。
対応ロケールとURLを先に固定し、翻訳、地域書式、通知、アクセシビリティ、SEOの順に同じ画面へ積み上げると、確認漏れを切り分けやすくなります。
参考リンク
- Laravel公式:Localization
- Laravel公式:Validation
- Laravel公式:Mail
- Carbon Documentation
- PHPマニュアル:NumberFormatter
- Unicode CLDR
- MDN:HTMLの
lang属性 - W3C:Language of Parts
- MDN:CSS論理プロパティ
- Google検索セントラル:多言語サイトと多地域サイト
- Google:
hreflangのガイド - MDN:
track要素 - W3C WAI:画像の代替テキスト
この記事に関連する株式会社greedenの取り組み
多言語対応は、翻訳だけでなくURL、表示形式、操作性を一つの要件として設計し、公開後も検証を続ける仕事です。株式会社greedenは、Laravelを含むWebシステム開発を、要件定義から実装、テスト、保守改善まで一貫して支援しています。
