【現場完全ガイド】Laravelの多言語・多地域化(i18n/L10n)――翻訳設計、日付/数値/通貨、URL/ミドルウェア、SEO/メール、RTL対応、アクセシブルな言語切替
この記事で学べること(要点)
- 翻訳資産の構造化(キー式/JSON式、階層・命名・粒度)と
trans_choiceを用いた複数形対応 - ロケール決定の戦略(URL/サブドメイン/セッション/
Accept-Language)とミドルウェアによる実装 - 日付・時刻・タイムゾーン(Carbon)、数値・通貨(PHP Intl/ICU)を文化圏ごとに正確に表示する方法
- メール/通知/バリデーションメッセージの多言語化、画像の代替テキスト・字幕の多言語化
- RTL(右から左)言語、フォント、色/アイコンの文化差に配慮したデザイン
- hreflang・メタデータ・サイトマップなどの多言語SEO、テスト/監視/運用の型
- アクセシブルな言語切替UI、本文中の言語切替(language of parts)、読み上げ最適化
想定読者(だれが得をする?)
- Laravel 初〜中級エンジニア:既存アプリを段階的に多言語対応へ拡張したい方
- SaaS/メディア/ECのテックリード:翻訳資産の管理運用と性能を両立したい方
- デザイナー/ライター/ローカライザー:文化差・表記揺れを抑えつつ分かりやすい文言を作りたい方
- QA/アクセシビリティ担当:読み上げ・キーボード・言語属性の検証ポイントを体系化したい方
アクセシビリティレベル:★★★★★
lang/dir/language-of-parts、言語切替UIのフォーカス/読み上げ、代替テキスト/字幕の多言語化、数字/日付の地域表記、色だけに依存しない文化差対応まで実装観点で網羅。
1. はじめに:i18n/L10nは「翻訳」だけではありません
多言語化は文字列の置換だけでは不十分です。
- 言語(ja/en/fr…)と地域(ja-JP/en-US/fr-CA)で表記や単位が変わります。
- 日付・時刻・曜日、数値・通貨・割合、書式(区切り/小数点)、長さ/重さなどを文化圏に合わせます。
- アクセシビリティでは、ページや要素に適切な
langを付け、読み上げの発音・辞書を切り替えます。 - SEOは hreflang、URL、サイトマップで地域別に正しくインデックスされるようにします。
Laravel は翻訳・ロケール・バリデーション文言・メールの多言語化の土台と、Carbon/PHP Intl による地域化(L10n)の連携が得意です。本記事は、段階導入できる現場の設計とコードをまとめます。
2. 翻訳資産の設計:キー式・JSON式・命名規約
2.1 ディレクトリ基本
resources/lang/
├─ en/
│ ├─ auth.php
│ ├─ validation.php
│ └─ app.php // アプリ共通文言
├─ ja/
│ ├─ auth.php
│ ├─ validation.php
│ └─ app.php
└─ en.json // JSON式(キー=原文)
ja.json
- キー式(配列):
__('app.welcome')のようにキーで参照。構造化と差分管理に強い。 - JSON式:
__('Sign in')の原文キー方式。既存UIの素早い部分翻訳に便利。 - チーム運用では、キー式を主、一時的に JSON を併用が扱いやすいです。
2.2 命名と粒度
- 画面やドメイン単位でファイルを分ける:
app.php,dashboard.php,orders.phpなど。 - キーは用途+意味で命名:
button.save,nav.settings,order.status.shipped。 - 完全文(例:「〇〇を保存しました」)は変数を使って再利用:
// resources/lang/ja/app.php return [ 'saved' => ':name を保存しました。', ]; // __('app.saved', ['name' => '設定'])
2.3 複数形と数値
// resources/lang/ja/app.php
return [
'items' => '{0} アイテムはありません|{1} 1アイテム|[2,*] :count アイテム',
];
trans_choice('app.items', 0); // アイテムはありません
trans_choice('app.items', 1); // 1アイテム
trans_choice('app.items', 5); // 5 アイテム
- ICUの複数形ルールが必要な場合は、ICU MessageFormat ライブラリの併用も検討。
3. ロケール決定戦略:URL/サブドメイン/セッション/ヘッダ
3.1 ルーティング層でのロケール前置
// routes/web.php
Route::group([
'prefix' => '{locale}',
'where' => ['locale' => 'ja|en'],
'middleware' => ['set.locale'],
], function () {
Route::get('/', [HomeController::class,'index'])->name('home');
// ... ほかのルート
});
3.2 ミドルウェアでロケール設定
// app/Http/Middleware/SetLocale.php
class SetLocale {
public function handle($request, Closure $next) {
$locale = $request->route('locale')
?? $request->session()->get('locale')
?? $this->fromAcceptLanguage($request) // 任意
?? config('app.locale');
app()->setLocale($locale);
Carbon\Carbon::setLocale($locale);
return $next($request);
}
protected function fromAcceptLanguage($request): ?string {
$supported = ['ja','en'];
$header = $request->header('Accept-Language'); // 例: "ja,en;q=0.8"
foreach (explode(',', (string)$header) as $lang) {
$code = strtolower(substr(trim($lang),0,2));
if (in_array($code, $supported, true)) return $code;
}
return null;
}
}
3.3 戦略の比較
- URLプレフィックス(
/ja/...):明示的でSEOに強い。ブックマーク/共有に向く。 - サブドメイン(
ja.example.com):運用・CDN分離に向く。 - セッションのみ:手軽だが、URLから文脈が見えず、SEO効果が薄い。
Accept-Language:初期推定に使い、最終はURL/セッションで再現性を持たせる。
4. ビューとUI:lang/dir、言語切替、アクセシビリティ
4.1 HTMLとページヘッダ
<!doctype html>
<html lang="{{ str_replace('_','-',app()->getLocale()) }}" dir="@rtl(en) ? 'ltr' : 'ltr'">
<head>
<meta charset="utf-8">
<title>@yield('title') – {{ config('app.name') }}</title>
<link rel="alternate" href="{{ localized_url('ja') }}" hreflang="ja">
<link rel="alternate" href="{{ localized_url('en') }}" hreflang="en">
</head>
langは ページ全体の主言語 を指定。- RTL 言語(ar, he 等)を扱うなら
dir="rtl"を適切に。混在箇所は要素単位でdirを切替。
4.2 言語切替UI(アクセシブル)
<nav aria-label="@lang('app.language_switcher')">
<ul class="inline-flex gap-2">
<li>
<a href="{{ localized_url('ja') }}"
hreflang="ja" lang="ja"
aria-current="{{ app()->getLocale()==='ja' ? 'true':'false' }}">
日本語
</a>
</li>
<li>
<a href="{{ localized_url('en') }}"
hreflang="en" lang="en"
aria-current="{{ app()->getLocale()==='en' ? 'true':'false' }}">
English
</a>
</li>
</ul>
</nav>
- 現在選択中は
aria-current="true"を付与。 - リンクテキストに現地語を使い、
lang/hreflangも付ける。 - 切替後は同一ページのロケール版へ遷移し、フォーカスを見出しへ移動。
4.3 本文中の言語切替(language of parts)
<p>ブランド名は <span lang="en">Example Cloud</span> です。</p>
- 日本語本文中の固有名詞・英語の見出しなどは、要素に
langを付けると読み上げが改善。
5. 日付・時刻・タイムゾーン:Carbonで安全に
5.1 ロケールと表示
Carbon\Carbon::setLocale(app()->getLocale());
$dt = Carbon\Carbon::parse($order->created_at)->timezone('Asia/Tokyo');
$human = $dt->isoFormat('LLLL'); // 例: 2025年10月29日水曜日 13:05
$relative = $dt->diffForHumans(); // 例: 5分前
5.2 ユーザーごとのタイムゾーン
- DBはUTCで保存、出力直前にユーザーTZへ変換。
- プロファイル設定に
timezoneを持たせ、ミドルウェアまたはアクセサで適用。 - 日付入力(予約/締切)はTZを明記し、相互確認の文言を追加。
6. 数値・通貨・単位:PHP Intl/ICUで地域化
6.1 NumberFormatter
$fmt = new \NumberFormatter(app()->getLocale(), \NumberFormatter::DECIMAL);
$fmt->setAttribute(\NumberFormatter::FRACTION_DIGITS, 2);
$price = $fmt->format(12345.6); // ja: 12,345.60 / fr: 12 345,60
6.2 通貨
$fmt = new \NumberFormatter(app()->getLocale(), \NumberFormatter::CURRENCY);
$price = $fmt->formatCurrency(1999.9, 'JPY'); // ¥1,999
6.3 単位/長さ/重さ
- 変換はサーバ側で統一。
km/mi切替は国/設定で判断。 - 表示は数値+単位を必ず併記。色やアイコン依存を避ける。
7. バリデーション/フォーム/メッセージの多言語化
7.1 バリデーションメッセージ
resources/lang/{locale}/validation.phpを整備。- 属性名は
attributesに自然言語で定義。
'attributes' => [
'email' => 'メールアドレス',
'password' => 'パスワード',
],
7.2 フォームの書式/プレースホルダ
- 住所、郵便番号、電話は国ごとに形式が異なるため、
help文を言語ごとに変更。 - 日付入力はISO形式を優先し、表示はロケール書式で。
7.3 エラーサマリの読み上げ
- 言語切替後のエラー文言は即時にロケール反映。
- サマリ見出しに
role="alert"と短文の指示を記載。
8. メール/通知/ドキュメント:多言語テンプレート運用
8.1 MailMessage のロケール
public function toMail($notifiable)
{
return (new MailMessage)
->locale($notifiable->preferred_locale ?? app()->getLocale())
->subject(__('mail.verify_subject'))
->line(__('mail.verify_body'))
->action(__('mail.verify_action'), $this->verificationUrl($notifiable));
}
8.2 Markdownメール
resources/views/vendor/mail/{locale}/に言語別テンプレを配置可能。- 画像の
altも言語別に。
8.3 PDF/レポート
- 目次/見出し/代替テキストを各言語に。
- 数値/日付/通貨はIntlで整形してから埋め込む。
9. 画像/動画の多言語化:代替テキスト・字幕
- 画像の
altは言語別に保存(メタJSONにalt[ja]/alt[en]のように)。 - 動画は
<track kind="captions" srclang="ja">を複数用意。 - 文化依存のアイコン(例:手振り/記号)は文言説明を併記。
10. RTL(右→左)対応:CSS/レイアウト/アイコン
- HTMLに
dir="rtl"、コンポーネント単位ではdirを切替。 - CSSは論理プロパティを優先(
margin-inline-start/text-align:start)。 - 方向性のあるアイコン(矢印など)はRTLで反転版を用意。
- 数字はラテン数字を基本にしつつ、地域によっては現地数字も検討。
11. URL/SEO:hreflang、メタ、サイトマップ
11.1 hreflang
<link rel="alternate" href="{{ localized_url('ja') }}" hreflang="ja">
<link rel="alternate" href="{{ localized_url('en') }}" hreflang="en">
<link rel="alternate" href="{{ localized_url('ja') }}" hreflang="x-default">
x-defaultは言語未指定ユーザー向け代表。- 各ロケールページが相互に参照すること。
11.2 タイトル/ディスクリプション
- 言語別の
<title>/meta descriptionを翻訳。 - Open Graph/Twitterカードも言語別に。
11.3 サイトマップ
- ロケール別URLを
xhtml:link rel="alternate" hreflang="..."で相互に記載。
12. ルーティング/生成:localized_url() ヘルパ
if (! function_exists('localized_url')) {
function localized_url(string $locale, ?string $name = null, array $params = []): string {
$name = $name ?? \Illuminate\Support\Facades\Route::currentRouteName();
$params = array_merge(\Illuminate\Support\Facades\Route::current()->parameters(), $params, ['locale'=>$locale]);
return route($name, $params);
}
}
- 現在ルートのロケール違いURLを一貫して生成。
13. 例:多言語トップページ
@extends('layouts.app')
@section('title', __('app.home'))
@section('content')
<h1 id="page-title" class="text-2xl font-semibold" tabindex="-1">
{{ __('app.welcome_title') }}
</h1>
<p>{{ __('app.welcome_body') }}</p>
<section aria-labelledby="features-title">
<h2 id="features-title">{{ __('app.features') }}</h2>
<ul>
<li>{{ __('app.feature_fast') }}</li>
<li>{{ __('app.feature_secure') }}</li>
<li>{{ __('app.feature_accessible') }}</li>
</ul>
</section>
<nav aria-label="{{ __('app.language_switcher') }}" class="mt-6">
<a href="{{ localized_url('ja') }}" lang="ja" hreflang="ja"
class="underline" aria-current="{{ app()->getLocale()==='ja' ? 'true':'false' }}">日本語</a>
<span aria-hidden="true"> | </span>
<a href="{{ localized_url('en') }}" lang="en" hreflang="en"
class="underline" aria-current="{{ app()->getLocale()==='en' ? 'true':'false' }}">English</a>
</nav>
@endsection
14. テスト:Feature/Browser/アクセシビリティ
14.1 Feature(ロケール切替)
public function test_locale_via_prefix()
{
$this->get('/ja')->assertSee('ようこそ');
$this->get('/en')->assertSee('Welcome');
}
public function test_mail_uses_user_locale()
{
$user = User::factory()->create(['preferred_locale'=>'ja']);
Notification::fake();
$user->notify(new VerifyEmail());
Notification::assertSentTo($user, VerifyEmail::class, function($n, $channels) {
return $n->toMail($user)->locale === 'ja';
});
}
14.2 Dusk(読み上げ/切替UI)
- ページの
<html lang>が現在のロケールと一致。 - 言語切替後、見出しへフォーカスが戻る。
- エラーサマリ/ボタン文言が選択言語で表示。
- RTLの画面で矢印/アラインが適切に反転。
15. 運用:翻訳フロー、バージョン管理、パフォーマンス
- 翻訳ファイルはコードと同じリポジトリで管理。キーの削除/追加はPRでトレース。
- 未翻訳検出:スタブ(
__PLACEHOLDER__)やCIで未定義キーを警告。 - 量が多い場合は翻訳管理ツール(用語集/レビュー)とJSONエクスポートで連携。
- キャッシュ:
php artisan config:cacheのほか、翻訳読み込みをOPcacheで恩恵。 - サーバ側で
intl(ICU)を最新安定版にし、ロケールデータ差を吸収。
16. よくある落とし穴と回避策
- 原文ハードコード → キー式へ移行、UIコピーを設計資産として扱う。
- URLにロケールなし → 共有/SEOが弱い。
/ja//enを導入。 lang未指定 → 読み上げの発音が不自然。<html lang>を必ず。- 日付/通貨の手書き整形 → Carbon/Intl を利用。
- 写真の
altを未翻訳 → 言語別メタで保存・表示。 - RTL非対応のCSS → 論理プロパティを使い、必要箇所のみ物理指定。
- 切替UIが画像や旗アイコンのみ → テキストで言語名を現地語表記。
Accept-Languageのみに依存 → URL/セッションで再現可能に。- 画像/色で文化差を誤解 → 文言で補足し、色依存を避ける。
17. チェックリスト(配布用)
翻訳資産
- [ ] キー式を基本に、命名/階層ルールがある
- [ ]
trans_choiceで複数形を正しく処理 - [ ] 未翻訳検知の仕組み(CI/ルール)を運用
ロケール決定
- [ ]
/ja//en等のURL戦略を採用 - [ ] ミドルウェアで
app()->setLocale()、Carbonにも反映 - [ ] 初期推定に
Accept-Languageを使用し、最終はURL/セッションで固定
表示/UI
- [ ]
<html lang>と必要箇所の language-of-parts - [ ] 言語切替UIは現地語表記、
aria-currentで状態を示す - [ ] RTLは
dirと論理プロパティで対応
日付/数値/通貨
- [ ] UTC保存→ユーザーTZ表示
- [ ] Intlで数値/通貨を整形
- [ ] 書式(小数点/区切り)・単位を文化圏に合わせる
メール/通知/メディア
- [ ] Mail/Notificationに
.locale()を適用 - [ ] 画像
alt/字幕を多言語化 - [ ] PDF/OG/Twitterカードのメタも言語別
SEO
- [ ] hreflang/alternateリンク
- [ ] 言語別タイトル/description
- [ ] サイトマップにロケール相互リンク
アクセシビリティ
- [ ] エラーサマリ/ステータスの読み上げ
- [ ] 言語切替後のフォーカス復帰
- [ ] 色・アイコンだけに依存しない表現
18. まとめ
- 翻訳(i18n)は文言の管理、地域化(L10n)は表示の正しさ。両輪で品質が決まります。
- ロケールはURL/ミドルウェアで再現性を担保し、初期推定に
Accept-Language。 - 日付/時刻/通貨/数値はCarbon/Intlで文化圏に合わせ、メールやPDFも含めて一貫性を。
- 画像の代替テキスト・字幕・language-of-parts・RTL・切替UIで読み上げ/操作を保証。
- SEOは hreflang/alternate/サイトマップで検索エンジンに正しく伝える。
- 翻訳資産をコードと同等に扱い、CIで未翻訳と崩れを検知、段階的に改善しましょう。
参考リンク
- Laravel 公式
- 日付/数値/ICU
- HTML/アクセシビリティ
- 多言語SEO
- メディアの多言語化
