【実務完全ガイド】Laravelの多言語化・ローカライゼーション――翻訳ファイル、言語切替、URL設計、バリデーション、日付・通貨、アクセシブルな多言語UI
この記事で学べること(要点)
- Laravelで多言語対応を始めるときの基本設計
langディレクトリ、PHP配列翻訳、JSON翻訳の使い分け__()、@lang、trans_choice()を使った文言管理- 言語切替、URL設計、ミドルウェア、セッション保存の実装パターン
- バリデーションメッセージ、属性名、日付・時刻・通貨・数値のローカライズ
- 多言語サイトで起きやすいSEO、キャッシュ、テスト、運用の落とし穴
- アクセシブルな言語切替UI、読み上げ、
lang属性、方向性、分かりやすいエラー表示
想定読者
- Laravel 初〜中級エンジニア:日本語だけのアプリを英語や他言語へ広げたい方
- SaaS / EC / メディア運営者:海外ユーザーや多言語ユーザーに対応したい方
- テックリード:翻訳ファイル、URL、キャッシュ、テストをチームで標準化したい方
- デザイナー / ライター / アクセシビリティ担当:言語切替や多言語文言を、誰にでも分かりやすく整えたい方
アクセシビリティレベル:★★★★★
多言語化は、単に文言を翻訳するだけではありません。ページの lang 属性、言語切替リンクの分かりやすさ、読み上げ時の言語判定、日付や金額の表記、エラー文の自然さまで含めて、利用者が安心して操作できる設計が必要です。本記事では、実装とUIの両面からアクセシブルな多言語対応を整理します。
1. はじめに:多言語化は「翻訳」ではなく「体験の設計」です
Laravelで多言語対応を始めるとき、多くの方が最初に考えるのは「日本語の文言を英語に置き換えること」です。もちろん翻訳は大切ですが、それだけでは十分ではありません。多言語対応では、URL、フォーム、バリデーション、日付、時刻、通貨、メール、SEO、キャッシュ、アクセシビリティまで影響します。
たとえば、画面上の文言だけ英語にしても、エラーメッセージが日本語のままだと利用者は困ります。日付が 2026/05/13 のように表示されていても、国や地域によって読み取り方が変わります。通貨や小数点、桁区切りも地域によって異なります。さらに、スクリーンリーダーはページの lang 属性を参考に読み上げるため、言語設定が不正確だと発音や理解に影響が出ます。
つまり、多言語化は「文字列を差し替える作業」ではなく、「利用者の言語・文化・操作環境に合わせて、情報を正しく届ける設計」です。Laravelにはローカライゼーション機能が用意されていますので、まずは標準機能を正しく使い、必要に応じて運用ルールを足していくのが安心です。
2. まず決める:対応言語とURL設計
多言語化を始める前に、最初に決めたいのは対応言語とURL設計です。ここが曖昧だと、あとからルーティング、SEO、キャッシュ、リンク生成で苦労します。
代表的なURL設計は次の3つです。
2.1 パスに言語を含める
/ja/products
/en/products
/fr/products
もっとも分かりやすく、SEOや共有にも向いています。ページの言語がURLから分かるため、キャッシュや検索エンジンとの相性も良いです。実務では、この方式を第一候補にすることが多いです。
2.2 サブドメインで分ける
ja.example.com
en.example.com
ブランドや地域ごとに明確に分けたい場合に向いています。ただし、証明書、Cookie、セッション、ルーティング、インフラ設定が少し複雑になります。
2.3 セッションやCookieだけで切り替える
/products
URLは同じで、セッションやCookieに保存した言語で表示を変える方式です。小さな管理画面なら便利ですが、SEOや共有URLには弱くなります。ある人が英語表示で見ているURLを別の人へ送っても、相手は日本語で表示される可能性があります。
実務では、公開サイトやSEOが重要なサービスなら /ja/... や /en/... のようにURLへ言語を含める方式が扱いやすいです。管理画面やログイン後のSaaSでは、セッションやユーザー設定を使う方式も現実的です。
3. Laravelの基本設定:locale と fallback_locale
Laravelでは、アプリケーションの言語設定を config/app.php で管理します。
'locale' => env('APP_LOCALE', 'ja'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
.env では次のように設定できます。
APP_LOCALE=ja
APP_FALLBACK_LOCALE=en
locale は基本言語、fallback_locale は翻訳が見つからない場合の予備言語です。たとえば日本語が基本で、一部の翻訳が不足している場合に英語へ戻す、といった使い方ができます。
ただし、fallback があるからといって翻訳漏れを放置してよいわけではありません。画面の一部だけ日本語、別の部分だけ英語になると、利用者は混乱します。開発中やテストで翻訳漏れを見つけやすい仕組みを用意しておくと安心です。
4. 翻訳ファイルの置き場所:lang ディレクトリを準備します
Laravelの新しいアプリケーションでは、初期状態で lang ディレクトリが存在しない場合があります。その場合は、次のコマンドで公開できます。
php artisan lang:publish
代表的な構成は次のようになります。
lang/
├─ ja/
│ ├─ messages.php
│ ├─ validation.php
│ └─ auth.php
├─ en/
│ ├─ messages.php
│ ├─ validation.php
│ └─ auth.php
└─ ja.json
Laravelでは、PHP配列形式の翻訳ファイルと、JSON形式の翻訳ファイルを使えます。どちらも便利ですが、用途を分けると保守しやすくなります。
5. PHP配列翻訳とJSON翻訳の使い分け
5.1 PHP配列翻訳
lang/ja/messages.php
return [
'welcome' => 'ようこそ、:nameさん',
'profile_updated' => 'プロフィールを更新しました。',
'items_count' => ':count件の項目があります。',
];
使い方:
{{ __('messages.welcome', ['name' => $user->name]) }}
PHP配列形式は、カテゴリごとに文言を整理しやすいのが特徴です。
たとえば、認証なら auth.php、フォームなら forms.php、管理画面なら admin.php のように分けられます。
5.2 JSON翻訳
lang/ja.json
{
"Welcome": "ようこそ",
"Save": "保存する",
"Cancel": "キャンセル"
}
使い方:
{{ __('Save') }}
JSON翻訳は、短い共通文言に向いています。
一方で、文脈ごとの管理や階層化はしにくいため、大規模アプリではPHP配列形式を中心にする方が保守しやすいことが多いです。
5.3 実務でのおすすめ
- 画面固有・業務固有の文言:PHP配列
- 共通ボタンや短いラベル:JSON
- バリデーション:
validation.php - 認証まわり:
auth.php - メール:
mail.phpやemails.php
最初から細かく分けすぎる必要はありませんが、「どこに文言を置くか」のルールをチームで決めておくと、翻訳ファイルが散らかりにくくなります。
6. 翻訳の基本:__()、@lang、trans_choice()
Laravelで翻訳文字列を取得する基本は __() です。
<h1>{{ __('messages.welcome', ['name' => $user->name]) }}</h1>
Bladeでは @lang も使えます。
@lang('messages.profile_updated')
件数に応じて文言を変える場合は trans_choice() を使います。
echo trans_choice('messages.comments', $count, ['count' => $count]);
翻訳ファイル側:
return [
'comments' => '{0} コメントはありません|{1} :count件のコメントがあります|[2,*] :count件のコメントがあります',
];
日本語では英語のような単数・複数の変化が少ないですが、英語や他言語に展開する場合は、件数表現を最初から trans_choice() で扱っておくと安全です。
7. 言語切替の実装:ミドルウェアで現在の言語を決めます
URLに言語を含める場合、たとえば /ja/products のようなルートを考えます。
まず、対応言語を設定ファイルにまとめます。
// config/locales.php
return [
'supported' => ['ja', 'en'],
'default' => 'ja',
];
ミドルウェアを作ります。
php artisan make:middleware SetLocale
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
class SetLocale
{
public function handle(Request $request, Closure $next)
{
$locale = $request->route('locale');
if (! in_array($locale, config('locales.supported'), true)) {
abort(404);
}
App::setLocale($locale);
return $next($request);
}
}
ルート側では次のようにします。
Route::prefix('{locale}')
->middleware(['set.locale'])
->group(function () {
Route::get('/products', [ProductController::class, 'index'])
->name('products.index');
});
リンク生成では、現在の言語を渡します。
<a href="{{ route('products.index', ['locale' => app()->getLocale()]) }}">
{{ __('messages.products') }}
</a>
この方式では、URLから言語が明確に分かります。共有URLや検索エンジンにも伝わりやすく、公開サイトでは特に扱いやすい構成です。
8. セッションやユーザー設定で言語を保存する場合
ログイン後の管理画面やSaaSでは、ユーザーごとに言語設定を持つこともあります。
例として、ユーザーテーブルに locale を持たせます。
Schema::table('users', function (Blueprint $table) {
$table->string('locale', 10)->default('ja');
});
ログイン後のミドルウェアで設定します。
public function handle(Request $request, Closure $next)
{
if ($request->user()?->locale) {
App::setLocale($request->user()->locale);
}
return $next($request);
}
言語切替フォーム:
<form method="POST" action="{{ route('settings.locale.update') }}">
@csrf
@method('PATCH')
<label for="locale">{{ __('settings.language') }}</label>
<select id="locale" name="locale">
<option value="ja" @selected(app()->getLocale() === 'ja')>日本語</option>
<option value="en" @selected(app()->getLocale() === 'en')>English</option>
</select>
<button type="submit">{{ __('messages.save') }}</button>
</form>
ユーザー設定として保存すると、端末を変えても同じ言語で表示しやすくなります。
一方で、公開ページではURLに言語が出ないため、SEOや共有リンクでは注意が必要です。
9. 言語切替UI:アクセシビリティを最初から意識します
言語切替は、ただ国旗アイコンを置けばよいものではありません。国旗は国を表すものであり、言語を正確に表すとは限らないためです。たとえば英語は米国だけではありませんし、スペイン語も複数の国で使われます。
おすすめは、言語名をその言語自身で表示することです。
<nav aria-label="{{ __('messages.language_switcher') }}">
<ul>
<li>
<a href="{{ route('home', ['locale' => 'ja']) }}" lang="ja" hreflang="ja">
日本語
</a>
</li>
<li>
<a href="{{ route('home', ['locale' => 'en']) }}" lang="en" hreflang="en">
English
</a>
</li>
</ul>
</nav>
現在選択中の言語には aria-current を付けると分かりやすくなります。
<a
href="{{ route('home', ['locale' => 'ja']) }}"
lang="ja"
hreflang="ja"
@if(app()->getLocale() === 'ja') aria-current="true" @endif
>
日本語
</a>
言語切替UIでは、次の点を意識します。
- 国旗だけに頼らない
- 言語名をテキストで表示する
- 選択中の言語を明示する
- キーボードだけで操作できる
- 切替後、必要ならページ先頭や見出しへフォーカスを戻す
多言語化は、言語が分かる人だけでなく、支援技術を使う人にも正しく伝える必要があります。
10. HTMLの lang 属性:読み上げ品質に直結します
多言語対応で忘れやすいのが、HTMLの lang 属性です。
レイアウトファイルで現在の言語を反映します。
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<title>{{ $title ?? config('app.name') }}</title>
</head>
<body>
{{ $slot ?? '' }}
</body>
</html>
ページ全体が日本語なら lang="ja"、英語なら lang="en" です。
ページ内に別言語の文が混ざる場合は、その部分にも lang を付けます。
<p>
サービス名は <span lang="en">Accessible Laravel</span> です。
</p>
スクリーンリーダーは lang 属性を参考に発音を切り替えることがあります。
そのため、lang が適切でないと、英語が日本語読みされたり、日本語が不自然に読まれたりする可能性があります。
見た目には分かりにくいですが、アクセシビリティではとても重要な設定です。
11. バリデーションメッセージの多言語化
フォームの多言語化で特に重要なのが、バリデーションメッセージです。
Laravelでは lang/{locale}/validation.php を使って、メッセージを管理できます。
例:
return [
'required' => ':attribute は必須です。',
'email' => ':attribute はメールアドレスの形式で入力してください。',
'max' => [
'string' => ':attribute は :max 文字以内で入力してください。',
],
'attributes' => [
'name' => 'お名前',
'email' => 'メールアドレス',
'password' => 'パスワード',
],
];
FormRequest側で attributes() を使うこともできます。
public function attributes(): array
{
return [
'name' => __('forms.name'),
'email' => __('forms.email'),
];
}
ただし、ここで翻訳呼び出しが複雑になりすぎると見通しが悪くなるため、チームで方針を決めるとよいです。
基本的には、共通項目は validation.php の attributes に置き、画面固有の項目は FormRequest 側で補うと管理しやすいです。
12. エラー表示:翻訳後も分かりやすい構造を保ちます
多言語化すると、文言の長さが言語によって変わります。英語では短い文でも、ドイツ語やフランス語では長くなることがあります。日本語から英語へ翻訳した場合も、ボタン幅やエラーメッセージの行数が変わります。
エラー表示では、次のような構造を標準にします。
@if ($errors->any())
<div id="error-summary" role="alert" tabindex="-1" class="border p-3 mb-4">
<h2>{{ __('forms.error_summary_title') }}</h2>
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
入力欄側:
<label for="email">{{ __('forms.email') }}</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
aria-invalid="{{ $errors->has('email') ? 'true' : 'false' }}"
aria-describedby="{{ $errors->has('email') ? 'email-error' : 'email-help' }}"
>
<p id="email-help">{{ __('forms.email_help') }}</p>
@error('email')
<p id="email-error">{{ $message }}</p>
@enderror
翻訳した文言が長くなっても、構造が壊れないように、余白や折り返しを前提にしたデザインにします。
多言語UIでは、固定幅のボタンや、1行前提のラベルは避けた方が安全です。
13. 日付・時刻・タイムゾーン:言語だけでなく地域も考えます
多言語化では、日付と時刻も重要です。
LaravelではCarbonを使う場面が多いですが、表示形式を固定文字列にしてしまうと、地域差に対応しづらくなります。
悪い例:
{{ $post->published_at->format('Y/m/d H:i') }}
この形式は日本では読みやすいですが、英語圏では自然でない場合があります。
実務では、表示用のフォーマッタを用意すると整理しやすいです。
class DateFormatter
{
public function date($value): string
{
return match (app()->getLocale()) {
'ja' => $value->format('Y年n月j日'),
'en' => $value->format('F j, Y'),
default => $value->toDateString(),
};
}
}
Blade側:
{{ app(DateFormatter::class)->date($post->published_at) }}
時刻を扱う場合は、タイムゾーンも重要です。
ユーザーごとに timezone を持たせるなら、表示時に変換します。
$post->published_at
->timezone($user->timezone ?? config('app.timezone'))
->format('Y-m-d H:i');
アクセシビリティの観点では、曖昧な日付を避けることも大切です。
たとえば「明日」「来週」だけではなく、必要に応じて具体的な日付を併記すると、誤解が減ります。
14. 数値・通貨・単位:見た目だけでなく意味を揃えます
通貨や数値表記も地域によって異なります。
日本円なら ¥1,000、米ドルなら $1,000.00 のように、通貨ごとに小数や記号の扱いが変わります。
シンプルな例:
class MoneyFormatter
{
public function format(int $amount, string $currency = 'JPY'): string
{
return match ($currency) {
'JPY' => '¥' . number_format($amount),
'USD' => '$' . number_format($amount / 100, 2),
default => number_format($amount),
};
}
}
ただし、本格的に多通貨対応する場合は、内部で「最小通貨単位」を持つ、通貨コードを保存する、丸め方を統一するなど、設計が必要です。
表示だけ翻訳しても、請求や決済の金額が曖昧では危険です。
単位も同様です。
- km / miles
- kg / lb
- Celsius / Fahrenheit
対象地域によって必要な単位が変わる場合は、表示層だけでなくデータ設計も見直します。
15. メールの多言語化:送信時の言語を明確にします
メールは、Web画面とは違い、送信後に表示言語を変えられません。
そのため、送信時に「どの言語で送るか」を明確に決める必要があります。
基本はユーザーの locale を使います。
Mail::to($user->email)
->locale($user->locale)
->send(new WelcomeMail($user));
Mailable側のビューでも翻訳を使います。
<h1>{{ __('emails.welcome_title') }}</h1>
<p>{{ __('emails.welcome_body', ['name' => $user->name]) }}</p>
メールでは特に次を意識します。
- 件名も翻訳する
- HTML版とテキスト版の両方を用意する
- リンク文言を具体的にする
- 長い文章を短い段落に分ける
- 画像だけで情報を伝えない
メールは利用者の環境差が大きいため、Web画面以上にシンプルで明確な構造が大切です。
16. ルート名と翻訳URL:どこまで翻訳するかを決めます
多言語サイトでは、URLのパス自体を翻訳するかどうかも検討ポイントです。
翻訳しない例:
/ja/products
/en/products
翻訳する例:
/ja/shohin
/en/products
URLパスまで翻訳すると、利用者にとって自然になる場合があります。
一方で、ルーティング、リンク生成、リダイレクト、SEO、保守が複雑になります。
実務では、最初は /ja/products のように共通パスにして、必要性が高くなったら翻訳URLを検討する方が安全です。
特に管理画面では、URLを翻訳する必要はあまりありません。
公開サイトと管理画面で方針を分けるのも自然です。
17. SEO:hreflang と canonical を整理します
公開サイトを多言語化する場合、SEOの設計も必要です。
言語別ページがある場合は、hreflang を使って対応関係を示します。
<link rel="alternate" hreflang="ja" href="https://example.com/ja/products">
<link rel="alternate" hreflang="en" href="https://example.com/en/products">
<link rel="alternate" hreflang="x-default" href="https://example.com/">
canonicalも言語ごとに適切に設定します。
すべての言語ページを同じcanonicalへ向けると、検索エンジンが別言語ページを正しく扱えない可能性があります。
また、ページタイトル、メタディスクリプション、OGPも翻訳対象に含めます。
本文だけ翻訳して、メタ情報が日本語のまま残るケースは意外と多いので、チェックリストに入れておくと安心です。
18. キャッシュ:言語ごとにキーを分けます
多言語化とキャッシュは相性が良い一方で、キー設計を間違えると混線します。
たとえば日本語のトップページをキャッシュしたあと、英語ユーザーにも同じキャッシュが返ると大きな問題です。
キャッシュキーには、必ず言語を含めます。
$key = 'home:' . app()->getLocale();
$posts = Cache::remember($key, 300, function () {
return Post::published()->latest()->take(10)->get();
});
テナントやユーザー設定も関係する場合は、それも含めます。
$key = sprintf(
'dashboard:%s:tenant:%d:user:%d',
app()->getLocale(),
tenant()->id,
auth()->id()
);
多言語化では、「見た目は同じページでも、言語が違えば別キャッシュ」という前提を忘れないようにします。
19. 翻訳運用:翻訳ファイルは“コード”としてレビューします
翻訳ファイルは、単なる文言集ではありません。
画面の意味、エラーの伝え方、ボタンの行動、利用者の安心感に直結します。
そのため、翻訳ファイルもコードと同じようにレビュー対象にします。
運用で決めたいことは次の通りです。
- 文体を統一する
- ボタン文言のルールを決める
- エラー文は原因と次の行動を入れる
- 翻訳キーの命名規則を決める
- 未使用キーを定期的に整理する
- 翻訳漏れをテストやCIで検出する
たとえば、ボタン文言は「保存」より「保存する」の方が行動として分かりやすい場合があります。
英語でも Save、Update profile、Send invitation のように、文脈に応じた具体性が大切です。
短ければ良いとは限りません。利用者が何をするボタンか分かることを優先します。
20. テスト:翻訳漏れとUI崩れを防ぎます
多言語化では、通常のFeatureテストに加えて、言語別の表示テストがあると安心です。
20.1 日本語表示のテスト
public function test_homepage_is_displayed_in_japanese()
{
$response = $this->get('/ja/products');
$response->assertOk()
->assertSee('商品');
}
20.2 英語表示のテスト
public function test_homepage_is_displayed_in_english()
{
$response = $this->get('/en/products');
$response->assertOk()
->assertSee('Products');
}
20.3 未対応言語は404
public function test_unsupported_locale_returns_404()
{
$this->get('/xx/products')
->assertNotFound();
}
20.4 バリデーションメッセージのテスト
public function test_validation_message_is_localized()
{
$response = $this->post('/ja/contact', [
'email' => '',
]);
$response->assertSessionHasErrors('email');
}
Duskなどのブラウザテストでは、長い翻訳でボタンが崩れていないか、言語切替がキーボードで操作できるかも確認できます。
21. よくある落とし穴と回避策
21.1 画面文言だけ翻訳して、バリデーションが日本語のまま
フォーム体験が不自然になります。
validation.php と属性名まで翻訳しましょう。
21.2 lang 属性を更新していない
スクリーンリーダーの読み上げに影響します。
レイアウトで app()->getLocale() を反映します。
21.3 キャッシュキーに言語が入っていない
別言語のページが混ざる原因になります。
キーには必ず locale を含めます。
21.4 国旗だけで言語切替を表現する
国と言語は同じではありません。
言語名をテキストで表示します。
21.5 翻訳文が長くなってUIが崩れる
多言語UIでは文言長が変わる前提で余白や折り返しを設計します。
21.6 URL設計を後回しにする
あとから変更するとリダイレクトやSEO対応が大変です。
最初に /ja/... 方式かセッション方式かを決めます。
22. チェックリスト(配布用)
基本設計
- [ ] 対応言語を決めている
- [ ] URLに言語を含めるか、ユーザー設定で管理するか決めている
- [ ]
localeとfallback_localeを設定している
翻訳ファイル
- [ ]
lang:publishで翻訳ファイルを管理している - [ ] PHP配列翻訳とJSON翻訳の使い分けを決めている
- [ ] バリデーションメッセージと属性名を翻訳している
- [ ] メール件名と本文も翻訳対象にしている
UI / アクセシビリティ
- [ ]
<html lang="">が現在の言語になっている - [ ] 言語切替は国旗だけに頼っていない
- [ ] 選択中の言語を
aria-currentなどで示している - [ ] エラーサマリと入力欄の紐付けがある
- [ ] 翻訳文が長くなってもUIが崩れない
日付・数値
- [ ] 日付形式を言語や地域に合わせている
- [ ] タイムゾーンを考慮している
- [ ] 通貨や単位の表示ルールがある
SEO / キャッシュ
- [ ] 公開ページで
hreflangを設定している - [ ] メタタイトルと説明文も翻訳している
- [ ] キャッシュキーに locale を含めている
テスト
- [ ] 対応言語ごとの表示テストがある
- [ ] 未対応言語が404になる
- [ ] バリデーションメッセージの翻訳を確認している
- [ ] 言語切替UIをキーボードで操作できる
23. まとめ
Laravelの多言語化は、__() で文言を翻訳するところから始められます。しかし実務では、翻訳ファイル、URL設計、バリデーション、日付、通貨、メール、SEO、キャッシュ、テスト、アクセシビリティまで含めて考える必要があります。
まずは、対応言語とURL設計を決め、lang ディレクトリで文言を整理し、locale をミドルウェアで正しく設定しましょう。次に、バリデーションやメール、日付・数値の表示まで広げると、利用者にとって自然な体験へ近づきます。
特にアクセシビリティでは、lang 属性、言語切替のテキスト表示、エラー文の分かりやすさが重要です。多言語化は、海外対応のためだけではなく、多様な利用者に情報を正しく届けるための設計でもあります。小さく始めて、翻訳とUIを少しずつ育てていきましょう。

