php elephant sticker
Photo by RealToughCandy.com on Pexels.com
Table of Contents

【Complete On-the-Ground Guide】Laravel Internationalization & Regionalization (i18n/L10n) — Translation Design, Dates/Numbers/Currency, URLs/Middleware, SEO/Email, RTL Support, and an Accessible Language Switcher

What You’ll Learn (Key Points)

  • Structuring translation assets (key-based/JSON-based, hierarchy/naming/granularity) and pluralization with trans_choice
  • Locale decision strategies (URL/subdomain/session/Accept-Language) and implementation with middleware
  • How to display culturally correct dates/times/time zones (Carbon) and numbers/currency (PHP Intl/ICU)
  • Localizing emails/notifications/validation messages, multilingual alt text for images and captions for video
  • Design considerations for RTL (right-to-left) languages, fonts, and culture-driven differences in colors/icons
  • Multilingual SEO with hreflang, metadata, sitemaps; patterns for testing/monitoring/operations
  • An accessible language-switcher UI, language of parts within content, and screen-reader optimization

Intended Readers (Who Benefits?)

  • Laravel beginner–intermediate engineers: want to incrementally add multilingual support to an existing app
  • Tech leads for SaaS/media/EC: want to balance translation asset management/operations with performance
  • Designers/writers/localizers: want clear copy while controlling cultural differences and style drift
  • QA/accessibility specialists: want a systematic set of checks for screen reading/keyboard/lang attributes

Accessibility Level: ★★★★★

Covers lang/dir/language-of-parts, focus/announcements for the language switcher, multilingual alt text/captions, locale-specific formatting for numbers/dates, and culture-aware design that doesn’t rely on color alone—from an implementation perspective.


1. Introduction: i18n/L10n Is More Than “Translation”

Localization is not just swapping strings.

  • Language (ja/en/fr…) and region (ja-JP/en-US/fr-CA) affect notation and units.
  • Dates/times/days of week, numbers/currency/percentages, formats (grouping/decimal), length/weight vary by culture.
  • For accessibility, set appropriate lang on pages/elements to switch screen-reader pronunciation and dictionaries.
  • For SEO, use hreflang, URLs, and sitemaps so region versions get indexed correctly.

Laravel excels at i18n foundations (translations/locales/validation copy/emails) and L10n with Carbon and PHP Intl. This article compiles practical design and code you can roll out in stages.


2. Designing Translation Assets: Key-Based vs JSON-Based & Naming Rules

2.1 Base Directories

resources/lang/
├─ en/
│  ├─ auth.php
│  ├─ validation.php
│  └─ app.php         // App-wide copy
├─ ja/
│  ├─ auth.php
│  ├─ validation.php
│  └─ app.php
└─ en.json            // JSON-style (key = source string)
   ja.json
  • Key-based (arrays): reference like __('app.welcome'). Great for structure and diffs.
  • JSON-based: source-string keys like __('Sign in'). Handy for quick partial translations of an existing UI.
  • Operationally, use key-based as primary, optionally JSON as a stopgap.

2.2 Naming & Granularity

  • Split files by screen or domain: app.php, dashboard.php, orders.php, etc.
  • Name keys by purpose + meaning: button.save, nav.settings, order.status.shipped.
  • For full sentences (“Saved :name”), use variables for reuse:
    // resources/lang/en/app.php
    return [
      'saved' => 'Saved :name.',
    ];
    // __('app.saved', ['name' => 'Settings'])
    

2.3 Plurals & Numbers

// resources/lang/en/app.php
return [
  'items' => '{0} No items|{1} 1 item|[2,*] :count items',
];
trans_choice('app.items', 0);   // No items
trans_choice('app.items', 1);   // 1 item
trans_choice('app.items', 5);   // 5 items
  • If you need ICU plural rules, consider adding an ICU MessageFormat library.

3. Locale Decision Strategies: URL/Subdomain/Session/Header

3.1 Locale Prefix at the Routing Layer

// routes/web.php
Route::group([
  'prefix' => '{locale}',
  'where' => ['locale' => 'ja|en'],
  'middleware' => ['set.locale'],
], function () {
  Route::get('/', [HomeController::class,'index'])->name('home');
  // ... other routes
});

3.2 Set the Locale in Middleware

// app/Http/Middleware/SetLocale.php
class SetLocale {
  public function handle($request, Closure $next) {
    $locale = $request->route('locale')
      ?? $request->session()->get('locale')
      ?? $this->fromAcceptLanguage($request) // optional
      ?? 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'); // e.g., "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 Comparing Strategies

  • URL prefix (/ja/...): explicit and SEO-friendly. Great for bookmarks/sharing.
  • Subdomain (ja.example.com): good for ops/CDN separation.
  • Session only: easy but URL lacks context; weak for SEO.
  • Accept-Language: use for initial guess; finalize with URL/session for reproducibility.

4. Views & UI: lang/dir, Language Switching, Accessibility

4.1 HTML & Page Head

<!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 sets the page’s primary language.
  • For RTL languages (ar, he, etc.), set dir="rtl" appropriately; when mixed, switch dir on elements.

4.2 Accessible Language Switcher 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>
  • Add aria-current="true" to the current selection.
  • Use endonym labels (language names in their own language) with lang/hreflang.
  • After switching, navigate to the same page in the new locale and move focus to the heading.

4.3 Language of Parts Within Body Text

<p>The brand name is <span lang="en">Example Cloud</span>.</p>
  • In Japanese text, tag proper nouns/English phrases with lang to improve screen-reader output.

5. Dates/Times/Time Zones: Do It Safely with Carbon

5.1 Locale & Display

Carbon\Carbon::setLocale(app()->getLocale());
$dt = Carbon\Carbon::parse($order->created_at)->timezone('Asia/Tokyo');

$human = $dt->isoFormat('LLLL'); // e.g., Wednesday, October 29, 2025 13:05
$relative = $dt->diffForHumans(); // e.g., 5 minutes ago

5.2 Per-User Time Zones

  • Store in UTC, convert to the user’s TZ right before output.
  • Keep a timezone in user profiles; apply in middleware or accessors.
  • For date input (bookings/deadlines), state the TZ explicitly and add confirmation copy.

6. Numbers/Currency/Units: Localize with 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 Currency

$fmt = new \NumberFormatter(app()->getLocale(), \NumberFormatter::CURRENCY);
$price = $fmt->formatCurrency(1999.9, 'JPY'); // ¥1,999

6.3 Units/Length/Weight

  • Standardize conversions on the server. Switch km/mi by country/settings.
  • Always show number + unit; don’t rely solely on color or iconography.

7. Validation/Forms/Messages: Multilingual Support

7.1 Validation Messages

  • Provide resources/lang/{locale}/validation.php.
  • Define human-friendly attribute names in attributes:
'attributes' => [
  'email' => 'Email address',
  'password' => 'Password',
],

7.2 Forms: Formats/Placeholders

  • Address/postal code/phone vary by country; localize help text per language.
  • Prefer ISO input format, render in locale format.

7.3 Screen-Reader Error Summaries

  • After switching language, reflect the locale immediately in error text.
  • Add a summary heading with role="alert" and concise instructions.

8. Mail/Notifications/Documents: Operating Multilingual Templates

8.1 MailMessage Locale

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 Mail

  • Place per-language templates under resources/views/vendor/mail/{locale}/.
  • Localize image alt text as well.

8.3 PDFs/Reports

  • Translate TOC/headings/alt text for each language.
  • Format numbers/dates/currency with Intl before injecting.

9. Multilingual Media: Alt Text & Captions

  • Store image alt per language (e.g., metadata JSON with alt[en], alt[ja]).
  • Provide multiple <track kind="captions" srclang="ja"> for video.
  • Cultural icons (hand gestures/symbols) should include text explanations.

10. RTL (Right→Left) Support: CSS/Layout/Icons

  • Use dir="rtl" on HTML; switch dir at component level as needed.
  • Prefer CSS logical properties (margin-inline-start, text-align: start).
  • Provide mirrored versions of directional icons (arrows, carets) under RTL.
  • Default to Latin digits, but consider locale numerals where appropriate.

11. URL/SEO: hreflang, Meta, Sitemaps

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 targets users with unspecified language.
  • Ensure reciprocal references across locale pages.

11.2 Titles/Descriptions

  • Translate per-language <title>/meta description.
  • Also localize Open Graph/Twitter Cards.

11.3 Sitemaps

  • In XML sitemaps, cross-link locale URLs with xhtml:link rel="alternate" hreflang="...".

12. Routing/Generation: localized_url() Helper

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);
  }
}
  • Consistently generate the same route in another locale.

13. Example: Multilingual Top Page

@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. Testing: Feature/Browser/Accessibility

14.1 Feature (Locale Switching)

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 (Screen Reading/Switcher UI)

  • <html lang> matches the current locale.
  • After switching language, focus returns to the heading.
  • Error summaries/buttons use the selected language.
  • On RTL screens, arrows/alignment flip appropriately.

15. Operations: Translation Flow, VCS, Performance

  • Keep translation files in the same repo as code. Track key adds/removals via PRs.
  • Detect missing translations: use stubs (__PLACEHOLDER__) and CI warnings for undefined keys.
  • For large volumes, use a TMS (glossary/review) and JSON exports.
  • Caching: besides php artisan config:cache, translation loading benefits from OPcache.
  • Keep the server’s intl (ICU) up to date to align locale data.

16. Common Pitfalls & How to Avoid Them

  • Hardcoded source strings → migrate to key-based and treat UI copy as a design asset.
  • No locale in URLs → weak sharing/SEO; introduce /ja//en.
  • Missing lang → unnatural speech synthesis; always set <html lang>.
  • Hand-formatting dates/currency → use Carbon/Intl.
  • Untranslated image alt → store/show via per-language metadata.
  • CSS not RTL-ready → prefer logical properties and use physical ones only when necessary.
  • Language switcher uses only flags/images → text labels in endonyms.
  • Relying solely on Accept-Language → make it reproducible with URL/session.
  • Miscommunication via images/colors → supplement with text; avoid color-only cues.

17. Checklist (Handout)

Translation Assets

  • [ ] Key-based as the default with naming/hierarchy rules
  • [ ] Correct plural handling via trans_choice
  • [ ] CI or rules to catch missing translations

Locale Resolution

  • [ ] Adopt URL strategy like /ja /en
  • [ ] Use middleware for app()->setLocale() and propagate to Carbon
  • [ ] Use Accept-Language for initial guess; finalize with URL/session

Display/UI

  • [ ] <html lang> and language-of-parts where needed
  • [ ] Language switcher uses endonyms, aria-current indicates state
  • [ ] RTL handled via dir and logical properties

Dates/Numbers/Currency

  • [ ] Store UTC → display in user TZ
  • [ ] Format numbers/currency with Intl
  • [ ] Match formatting (decimal/grouping) and units to locale

Mail/Notifications/Media

  • [ ] Apply .locale() to Mail/Notification
  • [ ] Multilingual image alt/captions
  • [ ] Per-language metadata for PDFs/OG/Twitter Cards

SEO

  • [ ] hreflang/alternate links
  • [ ] Per-language titles/descriptions
  • [ ] Sitemaps with cross-linked locales

Accessibility

  • [ ] Screen-readable error summaries/status
  • [ ] Focus restoration after language switch
  • [ ] Don’t rely solely on color/icons

18. Summary

  • i18n = managing copy; L10n = correct representation. Quality depends on both.
  • Ensure reproducibility with URL/middleware; use Accept-Language only for initial hints.
  • Match dates/times/currency/numbers to culture via Carbon/Intl, consistently across emails and PDFs.
  • Guarantee reading/operation via alt text, captions, language-of-parts, RTL, and a robust switcher UI.
  • Tell search engines the truth with hreflang/alternate/sitemaps.
  • Treat translations like code: detect missing strings/layout regressions in CI and improve iteratively.

References

By greeden

Leave a Reply

Your email address will not be published. Required fields are marked *

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