php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

【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 lang apropiado 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>
  • lang establece el idioma principal de la página.
  • Para idiomas RTL (ar, he, etc.), establece dir="rtl"; en mezclas, cambia dir a 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 lang para 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 timezone en 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/mi por 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 alt de 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 alt de imagen por idioma (p. ej., metadatos JSON con alt[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; cambia dir a 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-default apunta a usuarios sin idioma especificado.
  • Asegura referencias recíprocas entre páginas de locales.

11.2 Títulos/descripciones

  • Traduce <title>/meta description por 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 intl e 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.
  • alt de 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-Language para conjetura inicial; consolidar con URL/sesión

Visualización/UI

  • [ ] <html lang> y “idioma de las partes” donde aplique
  • [ ] Selector con endónimos; aria-current indica estado
  • [ ] RTL gestionado con dir y 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-Language solo 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

por greeden

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

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