【Comprehensive Guide】Multilingual (i18n) and Accessibility in Laravel — From Language Switching to Date/Currency Localization and RTL Support
What you’ll learn (highlights first)
- Basics of Laravel internationalization (i18n):
resources/lang
directory, array vs JSON translations, when to usetrans
/__
/trans_choice
- Language switching design with routing/middleware (URL path / subdomain / cookie), implementation comparison and best practices
- Accessibility essentials: HTML
lang
/dir
,hreflang
, partiallang
inside pages - Localization of dates, times, numbers, and currency with Carbon and PHP Intl (
NumberFormatter
) - Safe support for right-to-left (RTL) languages (layout, components, icons)
- Practical project structure for verification, testing language switching and screen reader behavior with Laravel Dusk, and documentation rules for translation consistency
Intended readers (who benefits most?)
- Laravel beginners/intermediates: Want to add two languages (e.g., Japanese/English) quickly
- Tech leads at agencies/SaaS: Need scalable URL design, translation workflow, QA processes
- Designers/technical writers: Want consistent style, UI copy, alt text
- Accessibility/QA staff: Need structured testing of language declaration, screen reading, LTR/RTL
Accessibility level: ★★★★★
Covers HTML
lang
anddir
, partial language attributes,hreflang
, accessible language switch UI,aria-live
announcements, and RTL considerations (focus order, mirrored icons) with practical code.
1. Why i18n and Accessibility Together?
Multilingual support is not just about translating strings. URL/routing, screen structure, screen reader output, dates, and currencies all affect user experience. Laravel’s translation system (resources/lang
), middleware, and Blade templating make it an excellent fit to implement i18n and a11y in tandem.
Key principles:
- User choice of language always comes first; auto-detection is secondary.
- URLs should indicate the language (better for sharing, SEO, debugging).
- Always specify
lang
(anddir
when needed). - Images, icons, ordering, and notifications also encode cultural assumptions.
This guide takes you from minimal production-ready setup to scalable practices for larger projects. ♡
2. Where and How to Store Translations: Arrays vs JSON, Pluralization, Placeholders
2.1 Directory structure
resources/
└─ lang/
├─ en/
│ ├─ auth.php
│ ├─ pagination.php
│ ├─ validation.php
│ └─ app.php
├─ ja/
│ ├─ auth.php
│ ├─ pagination.php
│ ├─ validation.php
│ └─ app.php
├─ en.json
└─ ja.json
- Array translations (e.g.
app.php
): use named keys like__('app.save')
- JSON translations: use original text as the key like
__('Save')
→"Save": "保存"
- Great for quick start; later migrate to arrays for structure
2.2 Placeholders and pluralization
// en/app.php
return [
'welcome_user' => 'Welcome, :name!',
'cart_items' => '{0} Your cart is empty|{1} You have :count item|[2,*] You have :count items',
];
// ja/app.php
return [
'welcome_user' => ':name さん、ようこそ!',
'cart_items' => '{0} カートは空です|{1} :count 件の商品があります|[2,*] :count 件の商品があります',
];
<p>{{ __('app.welcome_user', ['name' => $user->name]) }}</p>
<p>{{ trans_choice('app.cart_items', $count, ['count' => $count]) }}</p>
- Use
trans_choice
for plurals. - Always keep placeholder names consistent (
name
,count
).
3. URL Design and Middleware: Where Do We Decide the Language?
3.1 Recommended: language code in URL path
Example: /ja/products
, /en/products
- Pros: Easy to share/bookmark, SEO-friendly, easy CDN cache separation
- Cons: Slightly longer URLs
3.2 Example configuration
// routes/web.php
use App\Http\Middleware\SetLocaleFromUrl;
Route::middleware(SetLocaleFromUrl::class)->group(function () {
Route::prefix('{locale}')
->where(['locale' => 'ja|en|ar'])
->group(function () {
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
});
});
// app/Http/Middleware/SetLocaleFromUrl.php
class SetLocaleFromUrl
{
public function handle($request, Closure $next)
{
$supported = ['ja','en','ar'];
$locale = $request->route('locale');
if (! in_array($locale, $supported)) {
return redirect("/ja".$request->getRequestUri());
}
app()->setLocale($locale);
\Carbon\Carbon::setLocale($locale);
return $next($request);
}
}
Alternatives
- Subdomain (
ja.example.com
): useful for country-specific delivery - Cookie: single-domain apps (weaker for SEO/sharing)
3.3 Blade layout with lang
and dir
@php
$locale = app()->getLocale();
$dir = in_array($locale, ['ar','he','fa']) ? 'rtl' : 'ltr';
@endphp
<html lang="{{ $locale }}" dir="{{ $dir }}">
- Always declare
lang
on<html>
- For RTL, add
dir="rtl"
- Language switch navigation should have
aria-label
,lang
,hreflang
- Announce current language change via
aria-live
4. Translation Management Rules
- Split by domain:
auth.php
,nav.php
,errors.php
- Keys = function + purpose:
nav.settings.account
- Avoid reusing sentences across contexts (tone can break).
- Review via Pull Requests (style, screen reader output).
- Tests:
assertSeeInOrder
to verify reading order.
5. Dates, Times, Numbers, Currency
5.1 Carbon for dates/relative time
Carbon::setLocale(app()->getLocale());
$dt = Carbon::parse('2025-08-27 15:30:00');
$long = $dt->isoFormat('LL');
$diff = $dt->diffForHumans();
isoFormat
= locale-appropriate displaydiffForHumans
= natural relative expressions
5.2 Intl NumberFormatter
$fmt = new \NumberFormatter(app()->getLocale(), \NumberFormatter::CURRENCY);
$price = $fmt->formatCurrency(1234.5, 'JPY');
- Culture-specific separators, symbols, decimals
- Always pair currency with locale
6. RTL Languages
- Use logical CSS properties:
margin-inline-start
- Mirror arrows/icons with
transform: scaleX(-1)
- Source order must match logical focus order
- Force direction where needed:
<span dir="ltr">ABC-123</span>
7. Language Switch UI
Avoid
- Flags (not equal to languages)
- ISO codes alone (
en
,ja
)
Use
- Endonyms:
日本語 / English / العربية
- Include
lang
,hreflang
aria-live
announcement of current language
8. Validation Messages
Translate field names in validation.php
:
'attributes' => ['email' => 'メールアドレス']
- Avoid double maintenance between
attributes()
and lang files - Use
role="alert"
andaria-invalid="true"
for accessible error output
9. SEO: hreflang
Specify alternate language URLs in <head>
:
<link rel="alternate" hreflang="ja" href="/ja/...">
<link rel="alternate" hreflang="en" href="/en/...">
<link rel="alternate" hreflang="x-default" href="/en/...">
Avoid forced redirects; suggest language on first visit only.
10. Minimal Example App
Example controllers, views, and translation files (see original doc).
11–16. Additional Topics
- Alt text, captions, transcripts for media
- Localized emails, PDFs, error pages
- Accessibility checklist (lang, dir, translations, SEO)
- Feature tests and Dusk E2E for switching & screen reader output
- Common pitfalls (e.g., mixing currency with wrong locale)
- Team workflows: treat dictionary + UI copy as product assets
17. Conclusion
- Use URLs + middleware for explicit control
- Array/JSON translations,
trans_choice
for plurals - Carbon/Intl for localized dates, numbers, currency
- Accessible UI: language switch with
aria-live
, RTL-safe layouts - Test & review processes to make dictionary and copy part of your product asset base
This is the foundation of “inclusive multilingual experiences”. Proper i18n × a11y lowers both language and context barriers, strengthening trust and attachment to your product. Start with these examples, and grow your Laravel app into one that truly speaks to everyone. ♡
Who benefits most
- SaaS/EC PMs: Standardize URL & dictionary ops for smooth cross-team workflows
- Tech leads: Incremental i18n rollout, QA templates for RTL/currency
- Designers/writers: Consistent UI copy, alt text, captions
- Accessibility/QA: Structured checklists for lang/dir/hreflang, RTL, live announcements