php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

【現場完全ガイド】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-starttext-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で未翻訳と崩れを検知、段階的に改善しましょう。

参考リンク

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)