【Guía completa en terreno】Internacionalización y regionalización en Laravel (i18n/L10n) — Diseño de traducciones, fechas/números/moneda, URLs/middleware, SEO/email, soporte RTL y un selector de idioma accesible
Lo que aprenderás (Puntos clave)
- Estructurar activos de traducción (basados en clave/basados en JSON, jerarquía/nombrado/granularidad) y pluralización con
trans_choice - Estrategias para decidir la localización (URL/subdominio/sesión/
Accept-Language) e implementación con middleware - Cómo mostrar fechas/horas/zonas horarias culturalmente correctas (Carbon) y números/moneda (PHP Intl/ICU)
- Localizar correos/notificaciones/mensajes de validación, texto alternativo multilingüe para imágenes y subtítulos para video
- Consideraciones de diseño para lenguas RTL (derecha→izquierda), tipografías y diferencias culturales en colores/iconos
- SEO multilingüe con hreflang, metadatos y sitemaps; patrones para pruebas/monitorización/operaciones
- Un UI de selector de idioma accesible, “idioma de las partes” dentro del contenido y optimización para lectores de pantalla
Lectores previstos (¿A quién beneficia?)
- Ingenieros Laravel principiante–intermedio: desean añadir soporte multilingüe de forma incremental a una app existente
- Tech leads para SaaS/medios/EC: quieren equilibrar gestión/operación de activos de traducción con rendimiento
- Diseñadores/redactores/localizadores: desean copys claros controlando diferencias culturales y deriva de estilo
- QA/especialistas de accesibilidad: buscan un conjunto sistemático de comprobaciones para lectores de pantalla/teclado/atributos de idioma
Nivel de accesibilidad: ★★★★★
Cubre
lang/dir/idioma de las partes, foco/anuncios para el selector de idioma, alt multilingüe/subtítulos, formateo específico por locale para números/fechas y un diseño sensible a la cultura que no depende solo del color—desde la implementación.
1. Introducción: i18n/L10n es más que “traducción”
La localización no es solo cambiar cadenas.
- Idioma (ja/en/fr…) y región (ja-JP/en-US/fr-CA) afectan notación y unidades.
- Fechas/horas/días de semana, números/moneda/porcentajes, formatos (miles/decimales), longitud/peso varían por cultura.
- Para accesibilidad, establece
langapropiado en páginas/elementos para cambiar pronunciación y diccionarios de lectores de pantalla. - Para SEO, usa hreflang, URLs y sitemaps para que las versiones regionales se indexen correctamente.
Laravel destaca en fundamentos i18n (traducciones/locales/validación/emails) y en L10n con Carbon y PHP Intl. Este artículo compila diseño y código prácticos que puedes desplegar por etapas.
2. Diseñando activos de traducción: Basado en clave vs basado en JSON y reglas de nombrado
2.1 Directorios base
resources/lang/
├─ en/
│ ├─ auth.php
│ ├─ validation.php
│ └─ app.php // Copia general de la app
├─ ja/
│ ├─ auth.php
│ ├─ validation.php
│ └─ app.php
└─ en.json // Estilo JSON (clave = string origen)
ja.json
- Basado en clave (arrays): referenciar como
__('app.welcome'). Excelente para estructura y diffs. - Basado en JSON: claves iguales al string origen como
__('Sign in'). Útil para traducción rápida parcial de una UI existente. - Operativamente, usa basado en clave como principal y JSON como salvavidas.
2.2 Nombrado y granularidad
- Divide archivos por pantalla o dominio:
app.php,dashboard.php,orders.php, etc. - Nombra claves por propósito + significado:
button.save,nav.settings,order.status.shipped. - Para frases completas (“Saved :name”), usa variables para reutilizar:
// resources/lang/en/app.php return [ 'saved' => 'Saved :name.', ]; // __('app.saved', ['name' => 'Settings'])
2.3 Plurales y números
// 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
- Si necesitas reglas ICU de plural, considera añadir una librería ICU MessageFormat.
3. Estrategias para decidir el locale: URL/Subdominio/Sesión/Cabecera
3.1 Prefijo de locale en el enrutado
// routes/web.php
Route::group([
'prefix' => '{locale}',
'where' => ['locale' => 'ja|en'],
'middleware' => ['set.locale'],
], function () {
Route::get('/', [HomeController::class,'index'])->name('home');
// ... otras rutas
});
3.2 Establecer el locale en 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) // opcional
?? 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'); // p. ej., "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 Comparando estrategias
- Prefijo en URL (
/ja/...): explícito y amigable para SEO. Ideal para marcadores/compartir. - Subdominio (
ja.example.com): bueno para operaciones/CDN separadas. - Solo sesión: fácil pero la URL carece de contexto; débil para SEO.
Accept-Language: úsalo para la primera conjetura; finaliza con URL/sesión para reproducibilidad.
4. Vistas y UI: lang/dir, cambio de idioma, accesibilidad
4.1 HTML y <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>
langestablece el idioma principal de la página.- Para idiomas RTL (ar, he, etc.), establece
dir="rtl"; en mezclas, cambiadira nivel de elemento.
4.2 UI accesible de selector de idioma
<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>
- Añade
aria-current="true"en la selección actual. - Usa endónimos (nombre del idioma en su propia lengua) con
lang/hreflang. - Tras cambiar, navega a la misma página en el nuevo locale y mueve el foco al encabezado.
4.3 Idioma de partes dentro del texto
<p>La marca es <span lang="en">Example Cloud</span>.</p>
- En texto japonés, etiqueta nombres propios/frases en inglés con
langpara mejorar el TTS del lector de pantalla (análogo en otros idiomas).
5. Fechas/horas/zonas: hazlo bien con Carbon
5.1 Locale y visualización
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 Zonas por usuario
- Almacena en UTC y convierte a la zona del usuario justo antes de mostrar.
- Guarda
timezoneen el perfil; aplica en middleware o accessors. - Para entrada de fechas (reservas/plazos), indica la zona horaria y añade texto de confirmación.
6. Números/moneda/unidades: localiza con 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 Moneda
$fmt = new \NumberFormatter(app()->getLocale(), \NumberFormatter::CURRENCY);
$price = $fmt->formatCurrency(1999.9, 'JPY'); // ¥1,999
6.3 Unidades/longitud/peso
- Estandariza conversiones en el servidor. Alterna
km/mipor país/ajustes. - Muestra siempre número + unidad; no dependas solo de color o iconografía.
7. Validación/formularios/mensajes: soporte multilingüe
7.1 Mensajes de validación
- Proporciona
resources/lang/{locale}/validation.php. - Define nombres de atributos legibles en
attributes:
'attributes' => [
'email' => 'Email address',
'password' => 'Password',
],
7.2 Formularios: formatos/placeholders
- Dirección/código postal/teléfono varían por país; localiza el texto de ayuda por idioma.
- Prefiere formato ISO de entrada y renderiza en formato de locale.
7.3 Resúmenes de error para lectores de pantalla
- Tras cambiar idioma, refleja el locale inmediatamente en los errores.
- Añade un encabezado resumen con
role="alert"e instrucciones concisas.
8. Correo/notificaciones/documentos: operar plantillas multilingües
8.1 Locale en 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 Mail Markdown
- Coloca plantillas por idioma bajo
resources/views/vendor/mail/{locale}/. - Localiza también el
altde imágenes.
8.3 PDFs/informes
- Traduce índice/encabezados/alt para cada idioma.
- Formatea números/fechas/moneda con Intl antes de inyectar.
9. Medios multilingües: alt y subtítulos
- Almacena
altde imagen por idioma (p. ej., metadatos JSON conalt[en],alt[ja]). - Proporciona múltiples
<track kind="captions" srclang="ja">para video. - Iconos culturales (gestos/símbolos) deben incluir explicaciones de texto.
10. Soporte RTL (derecha→izquierda): CSS/layout/iconos
- Usa
dir="rtl"en HTML; cambiadira nivel de componente cuando sea necesario. - Prefiere propiedades lógicas CSS (
margin-inline-start,text-align: start). - Proporciona versiones reflejadas de iconos direccionales (flechas, carets) en RTL.
- Por defecto dígitos latinos, pero considera numerales locales donde proceda.
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-defaultapunta a usuarios sin idioma especificado.- Asegura referencias recíprocas entre páginas de locales.
11.2 Títulos/descripciones
- Traduce
<title>/meta descriptionpor idioma. - Localiza también Open Graph/Twitter Cards.
11.3 Sitemaps
- En sitemaps XML, enlaza URLs de locales con
xhtml:link rel="alternate" hreflang="...".
12. Routing/generación: helper 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);
}
}
- Genera de forma consistente la misma ruta en otro locale.
13. Ejemplo: portada multilingüe
@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. Pruebas: Feature/navegador/accesibilidad
14.1 Feature (cambio de locale)
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 (lector de pantalla/selector)
<html lang>coincide con el locale actual.- Tras cambiar idioma, el foco vuelve al encabezado.
- Resúmenes de error/botones usan el idioma seleccionado.
- En pantallas RTL, flechas/alineación se invierten correctamente.
15. Operaciones: flujo de traducción, VCS, rendimiento
- Mantén los archivos de traducción en el mismo repo que el código. Controla altas/bajas de claves vía PRs.
- Detecta faltas: usa stubs (
__PLACEHOLDER__) y avisos en CI para claves indefinidas. - Para grandes volúmenes, usa un TMS (glosario/revisión) y exportes JSON.
- Caché: además de
php artisan config:cache, la carga de traducciones se beneficia de OPcache. - Mantén actualizados
intle ICU del servidor para alinear datos de locales.
16. Errores comunes y cómo evitarlos
- Strings hardcodeados → migra a basado en clave y trata el copy de UI como activo de diseño.
- Sin locale en URL → compartir/SEO débiles; introduce
/ja//en. - Falta
lang→ síntesis de voz antinatural; establece siempre<html lang>. - Fechas/moneda a mano → usa Carbon/Intl.
altde imágenes sin traducir → almacena/muestra vía metadatos por idioma.- CSS no listo para RTL → prefiere propiedades lógicas y usa físicas solo si es necesario.
- Selector solo con banderas/imágenes → etiquetas de texto en endónimos.
- Depender solo de
Accept-Language→ hazlo reproducible con URL/sesión. - Comunicación solo con imágenes/colores → acompaña con texto; evita pistas solo por color.
17. Lista de verificación (handout)
Activos de traducción
- [ ] Basado en clave por defecto con reglas de nombrado/jerarquía
- [ ] Plurales correctos vía
trans_choice - [ ] CI o reglas para detectar traducciones faltantes
Resolución de locale
- [ ] Estrategia URL tipo
/ja/en - [ ] Middleware para
app()->setLocale()y propagar a Carbon - [ ]
Accept-Languagepara conjetura inicial; consolidar con URL/sesión
Visualización/UI
- [ ]
<html lang>y “idioma de las partes” donde aplique - [ ] Selector con endónimos;
aria-currentindica estado - [ ] RTL gestionado con
diry propiedades lógicas
Fechas/números/moneda
- [ ] Almacenar en UTC → mostrar en TZ de usuario
- [ ] Formatear números/moneda con Intl
- [ ] Ajustar formato (decimales/miles) y unidades al locale
Correo/notificaciones/medios
- [ ] Aplicar
.locale()a Mail/Notification - [ ] Alt/subtítulos multilingües
- [ ] Metadatos por idioma para PDFs/OG/Twitter Cards
SEO
- [ ] hreflang/enlaces alternos
- [ ] Títulos/descripciones por idioma
- [ ] Sitemaps con locales enlazados cruzados
Accesibilidad
- [ ] Resúmenes/estados legibles por pantalla
- [ ] Restauración de foco tras cambiar idioma
- [ ] No depender solo de color/iconos
18. Resumen
- i18n = gestionar copy; L10n = representación correcta. La calidad depende de ambos.
- Garantiza reproducibilidad con URL/middleware; usa
Accept-Languagesolo como pista inicial. - Alinea fechas/horas/moneda/números a la cultura con Carbon/Intl, también en emails y PDFs.
- Asegura lectura/operación con alt/subtítulos, “idioma de las partes”, RTL y un selector robusto.
- Dile la verdad a los buscadores con hreflang/alternate/sitemaps.
- Trata las traducciones como código: detecta strings faltantes/regresiones de layout en CI y mejora iterativamente.
Referencias
- Documentación oficial de Laravel
- Fechas/números/ICU
- HTML/Accesibilidad
- SEO multilingüe
- Medios multilingües
