【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
langon 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>
langsets the page’s primary language.- For RTL languages (ar, he, etc.), set
dir="rtl"appropriately; when mixed, switchdiron 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
langto 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
timezonein 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/miby 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
helptext 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
alttext 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
altper language (e.g., metadata JSON withalt[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; switchdirat 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-defaulttargets 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-Languagefor initial guess; finalize with URL/session
Display/UI
- [ ]
<html lang>and language-of-parts where needed - [ ] Language switcher uses endonyms,
aria-currentindicates state - [ ] RTL handled via
dirand 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-Languageonly 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
- Laravel Official Docs
- Dates/Numbers/ICU
- HTML/Accessibility
- Multilingual SEO
- Multilingual Media
