Localización
Laravel proporciona un sistema completo de localización para crear aplicaciones multiidioma. Aprende a traducir textos, formatear fechas y números según el idioma del usuario.
¿Qué es la localización?
La localización (i18n) permite adaptar tu aplicación a diferentes idiomas y regiones. Esto incluye:
- Traducción de textos: menús, mensajes, etiquetas
- Formato de fechas: 16/12/2025 (España) vs 12/16/2025 (EEUU)
- Formato de números: 1.234,56 (España) vs 1,234.56 (EEUU)
- Pluralización: "1 comentario" vs "5 comentarios"
Configuración del idioma
El idioma por defecto se configura en config/app.php:
<?php
return [
// Idioma por defecto
'locale' => 'es',
// Idioma de respaldo si no existe la traducción
'fallback_locale' => 'en',
// Idiomas disponibles (para validación)
'available_locales' => ['es', 'en', 'fr', 'de'],
];
También puedes configurar el idioma en .env:
APP_LOCALE=es
APP_FALLBACK_LOCALE=en
Archivos de traducción
Las traducciones se almacenan en lang/.
Hay dos formatos disponibles:
Formato PHP (recomendado)
Crea un directorio por idioma con archivos PHP que devuelven arrays:
lang/
├── es/
│ ├── messages.php
│ ├── auth.php
│ └── validation.php
└── en/
├── messages.php
├── auth.php
└── validation.php
<?php
// lang/es/messages.php
return [
'welcome' => 'Bienvenido a nuestra aplicación',
'goodbye' => 'Hasta pronto',
// Grupos anidados
'nav' => [
'home' => 'Inicio',
'about' => 'Acerca de',
'contact' => 'Contacto',
],
'auth' => [
'login' => 'Iniciar sesión',
'logout' => 'Cerrar sesión',
'register' => 'Registrarse',
],
];
<?php
// lang/en/messages.php
return [
'welcome' => 'Welcome to our application',
'goodbye' => 'See you soon',
'nav' => [
'home' => 'Home',
'about' => 'About',
'contact' => 'Contact',
],
'auth' => [
'login' => 'Log in',
'logout' => 'Log out',
'register' => 'Register',
],
];
Formato JSON
Para traducciones simples, puedes usar archivos JSON
directamente en lang/:
// lang/es.json
{
"Welcome": "Bienvenido",
"Email": "Correo electrónico",
"Password": "Contraseña"
}
Usar traducciones
Usa el helper __() o la directiva
@lang en Blade:
<?php
// Archivos PHP: archivo.clave
echo __('messages.welcome');
// "Bienvenido a nuestra aplicación"
// Claves anidadas con notación de punto
echo __('messages.nav.home');
// "Inicio"
// Archivos JSON: la clave es el texto original
echo __('Welcome');
// "Bienvenido"
// Si no existe la traducción, devuelve la clave
echo __('messages.nonexistent');
// "messages.nonexistent"
En vistas Blade:
<!-- Usando el helper -->
<h1>{{ __('messages.welcome') }}</h1>
<!-- Usando la directiva @lang -->
<h1>@lang('messages.welcome')</h1>
<!-- Navegación -->
<nav>
<a href="/">{{ __('messages.nav.home') }}</a>
<a href="/about">{{ __('messages.nav.about') }}</a>
<a href="/contact">{{ __('messages.nav.contact') }}</a>
</nav>
Parámetros en traducciones
Puedes incluir variables en las traducciones usando
placeholders con :nombre:
<?php
// lang/es/messages.php
return [
'greeting' => 'Hola, :name',
'order_status' => 'Tu pedido #:number está :status',
'items_in_cart' => 'Tienes :count artículos en tu carrito',
];
<?php
// Pasar parámetros como segundo argumento
echo __('messages.greeting', ['name' => 'Carlos']);
// "Hola, Carlos"
echo __('messages.order_status', [
'number' => '12345',
'status' => 'en camino',
]);
// "Tu pedido #12345 está en camino"
Pluralización
Laravel maneja automáticamente las formas singular
y plural con trans_choice():
<?php
// lang/es/messages.php
return [
// Singular | Plural
'comments' => ':count comentario|:count comentarios',
// Con rangos específicos
'items' => '{0} No hay artículos|{1} Hay un artículo|[2,*] Hay :count artículos',
// Múltiples rangos
'progress' => '{0} Sin empezar|[1,25] Iniciando|[26,75] En progreso|[76,99] Casi listo|{100} Completado',
];
<?php
echo trans_choice('messages.comments', 1, ['count' => 1]);
// "1 comentario"
echo trans_choice('messages.comments', 5, ['count' => 5]);
// "5 comentarios"
echo trans_choice('messages.items', 0);
// "No hay artículos"
echo trans_choice('messages.items', 1);
// "Hay un artículo"
echo trans_choice('messages.items', 10, ['count' => 10]);
// "Hay 10 artículos"
Cambiar idioma dinámicamente
Puedes cambiar el idioma en tiempo de ejecución
con App::setLocale():
<?php
use Illuminate\Support\Facades\App;
// Obtener el idioma actual
$locale = App::getLocale(); // "es"
// Cambiar idioma
App::setLocale('en');
// Verificar si es un idioma específico
if (App::isLocale('es')) {
// ...
}
// Obtener el idioma de respaldo
$fallback = App::getFallbackLocale();
Middleware para cambio de idioma
Crea un middleware para establecer el idioma según la preferencia del usuario o la URL:
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
// Prioridad: sesión > usuario > navegador > defecto
$locale = $request->session()->get('locale')
?? $request->user()?->preferred_locale
?? $request->getPreferredLanguage(config('app.available_locales'))
?? config('app.locale');
// Validar que el idioma esté disponible
if (in_array($locale, config('app.available_locales', ['es', 'en']))) {
App::setLocale($locale);
}
return $next($request);
}
}
Registra el middleware en bootstrap/app.php:
<?php
use App\Http\Middleware\SetLocale;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
SetLocale::class,
]);
})
->create();
Selector de idioma
Crea una ruta para que el usuario cambie el idioma:
<?php
// routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/language/{locale}', function (Request $request, string $locale) {
// Validar idioma
if (! in_array($locale, config('app.available_locales', ['es', 'en']))) {
abort(404);
}
// Guardar en sesión
$request->session()->put('locale', $locale);
// Si el usuario está autenticado, guardar preferencia
if ($user = $request->user()) {
$user->update(['preferred_locale' => $locale]);
}
return redirect()->back();
})->name('language.switch');
Y el componente en Blade:
<!-- resources/views/components/language-switcher.blade.php -->
<div class="language-switcher">
@foreach(config('app.available_locales', ['es', 'en']) as $locale)
<a
href="{{ route('language.switch', $locale) }}"
class="{{ App::isLocale($locale) ? 'active' : '' }}"
>
{{ strtoupper($locale) }}
</a>
@endforeach
</div>
<!-- Uso: <x-language-switcher /> -->
Formateo de fechas
Carbon (incluido en Laravel) soporta localización para fechas:
<?php
use Carbon\Carbon;
// Configurar locale de Carbon
Carbon::setLocale('es');
$date = Carbon::now();
// Formato localizado
echo $date->isoFormat('dddd, D [de] MMMM [de] YYYY');
// "lunes, 16 de diciembre de 2025"
// Tiempo relativo
echo $date->diffForHumans();
// "hace 5 minutos"
// También funciona en modelos Eloquent
$post = Post::first();
echo $post->created_at->isoFormat('D MMM YYYY');
// "16 dic 2025"
echo $post->created_at->diffForHumans();
// "hace 2 días"
Para que Carbon use el mismo locale que Laravel, sincronízalo en tu middleware de idioma:
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Carbon\Carbon;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->session()->get('locale', config('app.locale'));
if (in_array($locale, config('app.available_locales', ['es', 'en']))) {
App::setLocale($locale);
// Sincronizar Carbon con el locale de Laravel
Carbon::setLocale($locale);
}
return $next($request);
}
}
Formateo de números y monedas
Laravel incluye la clase Number con
métodos para formatear números y monedas según el locale:
<?php
use Illuminate\Support\Number;
// Formato de moneda (usa USD por defecto)
echo Number::currency(1000);
// "$1,000.00"
// Especificar moneda
echo Number::currency(1000, in: 'EUR');
// "€1,000.00"
// Especificar moneda y locale
echo Number::currency(1000, in: 'EUR', locale: 'es');
// "1.000,00 €"
echo Number::currency(1000, in: 'EUR', locale: 'de');
// "1.000,00 €"
// Controlar decimales
echo Number::currency(1000, in: 'EUR', locale: 'es', precision: 0);
// "1.000 €"
Otros métodos útiles de Number:
<?php
use Illuminate\Support\Number;
// Formato de número con separadores
echo Number::format(1234567.89, locale: 'es');
// "1.234.567,89"
echo Number::format(1234567.89, locale: 'en');
// "1,234,567.89"
// Porcentajes
echo Number::percentage(75.5, locale: 'es');
// "75,50 %"
// Abreviar números grandes
echo Number::abbreviate(1500000);
// "1.5M"
// Tamaño de archivos
echo Number::fileSize(1024 * 1024);
// "1 MB"
// Números ordinales
echo Number::ordinal(1); // "1st"
echo Number::ordinal(21); // "21st"
Puedes establecer una moneda por defecto:
<?php
use Illuminate\Support\Number;
// Establecer moneda por defecto (en AppServiceProvider)
Number::useCurrency('EUR');
// Ahora todas las llamadas usan EUR
echo Number::currency(1000, locale: 'es');
// "1.000,00 €"
// O usar una moneda temporalmente
$price = Number::withCurrency('GBP', function () {
return Number::currency(1000);
});
// "£1,000.00"
Validación localizada
Laravel incluye traducciones de mensajes de validación. Publica los archivos de idioma:
php artisan lang:publish
Esto crea lang/en/validation.php. Copia
y traduce para otros idiomas:
<?php
// lang/es/validation.php
return [
'required' => 'El campo :attribute es obligatorio.',
'email' => 'El campo :attribute debe ser una dirección de correo válida.',
'min' => [
'string' => 'El campo :attribute debe tener al menos :min caracteres.',
'numeric' => 'El campo :attribute debe ser al menos :min.',
],
'max' => [
'string' => 'El campo :attribute no debe superar los :max caracteres.',
],
// Nombres de atributos personalizados
'attributes' => [
'name' => 'nombre',
'email' => 'correo electrónico',
'password' => 'contraseña',
'phone' => 'teléfono',
],
];
Tip: Puedes encontrar traducciones de validación para muchos idiomas en Laravel-Lang/lang.
Resumen
- Configura el idioma en
config/app.phpo.env - Las traducciones van en
lang/en formato PHP o JSON - Usa
__()o@langpara traducir textos - Los placeholders (
:name) permiten variables dinámicas trans_choice()maneja pluralización- Crea un middleware para cambiar idioma según preferencia del usuario
- Carbon soporta fechas localizadas con
isoFormat() Number::currency()formatea monedas según locale
Ejercicios
Ejercicio 1: Archivo de traducciones para e-commerce
Crea un archivo de traducciones lang/es/shop.php
con mensajes para una tienda: añadir al carrito,
proceder al pago, pedido confirmado, etc. Incluye
mensajes con parámetros para nombre de producto y precio.
Ver solución
<?php
// lang/es/shop.php
return [
'cart' => [
'add' => 'Añadir al carrito',
'remove' => 'Eliminar del carrito',
'empty' => 'Tu carrito está vacío',
'added' => ':product añadido al carrito',
'total' => 'Total: :price',
],
'checkout' => [
'title' => 'Finalizar compra',
'proceed' => 'Proceder al pago',
'confirm' => 'Confirmar pedido',
'cancel' => 'Cancelar',
],
'order' => [
'confirmed' => 'Pedido #:number confirmado',
'status' => 'Estado del pedido: :status',
'thank_you' => 'Gracias por tu compra, :name',
],
'product' => [
'price' => 'Precio: :amount',
'stock' => ':count unidades disponibles',
'out_of_stock' => 'Agotado',
],
// Pluralización
'items' => '{0} No hay productos|{1} 1 producto|[2,*] :count productos',
];
// lang/en/shop.php
return [
'cart' => [
'add' => 'Add to cart',
'remove' => 'Remove from cart',
'empty' => 'Your cart is empty',
'added' => ':product added to cart',
'total' => 'Total: :price',
],
'checkout' => [
'title' => 'Checkout',
'proceed' => 'Proceed to payment',
'confirm' => 'Confirm order',
'cancel' => 'Cancel',
],
'order' => [
'confirmed' => 'Order #:number confirmed',
'status' => 'Order status: :status',
'thank_you' => 'Thank you for your purchase, :name',
],
'product' => [
'price' => 'Price: :amount',
'stock' => ':count units available',
'out_of_stock' => 'Out of stock',
],
'items' => '{0} No products|{1} 1 product|[2,*] :count products',
];
Ejercicio 2: Middleware de idioma por URL
Crea un middleware que detecte el idioma desde el
primer segmento de la URL (/es/products,
/en/products). Si el segmento no es un
idioma válido, usa el idioma por defecto.
Ver solución
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Response;
class SetLocaleFromUrl
{
private array $availableLocales = ['es', 'en', 'fr', 'de'];
public function handle(Request $request, Closure $next): Response
{
// Obtener primer segmento de la URL
$segment = $request->segment(1);
if ($segment && in_array($segment, $this->availableLocales)) {
App::setLocale($segment);
} else {
App::setLocale(config('app.locale', 'es'));
}
return $next($request);
}
}
// routes/web.php
use Illuminate\Support\Facades\Route;
// Rutas con prefijo de idioma
Route::prefix('{locale}')
->where(['locale' => 'es|en|fr|de'])
->middleware('locale.url')
->group(function () {
Route::get('/products', [ProductController::class, 'index'])
->name('products.index');
Route::get('/products/{product}', [ProductController::class, 'show'])
->name('products.show');
});
// Redirección a idioma por defecto
Route::get('/', function () {
return redirect('/' . config('app.locale'));
});
Ejercicio 3: Helper de formato de fechas localizado
Crea una función helper format_date()
que reciba una fecha y un formato, y devuelva la fecha
formateada según el idioma actual de la aplicación.
Incluye formatos predefinidos: 'short', 'medium', 'long'.
Ver solución
<?php
// app/Helpers/functions.php
use Carbon\Carbon;
use Illuminate\Support\Facades\App;
if (! function_exists('format_date')) {
function format_date(
Carbon|string|null $date,
string $format = 'medium'
): string {
if ($date === null) {
return '';
}
if (is_string($date)) {
$date = Carbon::parse($date);
}
// Asegurar que Carbon use el locale actual
$date->locale(App::getLocale());
// Formatos predefinidos según idioma
$formats = [
'es' => [
'short' => 'D/M/YY', // 16/12/25
'medium' => 'D MMM YYYY', // 16 dic 2025
'long' => 'D [de] MMMM [de] YYYY', // 16 de diciembre de 2025
'full' => 'dddd, D [de] MMMM [de] YYYY', // lunes, 16 de diciembre de 2025
],
'en' => [
'short' => 'M/D/YY', // 12/16/25
'medium' => 'MMM D, YYYY', // Dec 16, 2025
'long' => 'MMMM D, YYYY', // December 16, 2025
'full' => 'dddd, MMMM D, YYYY', // Monday, December 16, 2025
],
];
$locale = App::getLocale();
$localeFormats = $formats[$locale] ?? $formats['es'];
// Usar formato predefinido o el proporcionado
$isoFormat = $localeFormats[$format] ?? $format;
return $date->isoFormat($isoFormat);
}
}
if (! function_exists('format_datetime')) {
function format_datetime(
Carbon|string|null $date,
string $format = 'medium'
): string {
if ($date === null) {
return '';
}
if (is_string($date)) {
$date = Carbon::parse($date);
}
$date->locale(App::getLocale());
$formats = [
'es' => [
'short' => 'D/M/YY H:mm',
'medium' => 'D MMM YYYY, H:mm',
'long' => 'D [de] MMMM [de] YYYY [a las] H:mm',
],
'en' => [
'short' => 'M/D/YY h:mm A',
'medium' => 'MMM D, YYYY, h:mm A',
'long' => 'MMMM D, YYYY [at] h:mm A',
],
];
$locale = App::getLocale();
$localeFormats = $formats[$locale] ?? $formats['es'];
$isoFormat = $localeFormats[$format] ?? $format;
return $date->isoFormat($isoFormat);
}
}
// Uso en Blade:
// {{ format_date($post->created_at) }}
// "16 dic 2025"
// {{ format_date($post->created_at, 'long') }}
// "16 de diciembre de 2025"
// {{ format_datetime($order->placed_at, 'medium') }}
// "16 dic 2025, 14:30"
¿Has encontrado un error o tienes una sugerencia para mejorar esta lección?
Escríbenos¿Quieres dominar la localización en Laravel?
Aprende a crear aplicaciones multi-idioma completas sin dependencias externas en nuestro curso premium.
Ver curso de Multi-idioma