【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/langdirectory, 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, partiallanginside 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
langanddir, partial language attributes,hreflang, accessible language switch UI,aria-liveannouncements, 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(anddirwhen 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_choicefor 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
langon<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:
assertSeeInOrderto 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-liveannouncement 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_choicefor 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
