Lección 38 de 45 12 min de lectura

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
<?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:

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:

text
lang/
├── es/
│   ├── messages.php
│   ├── auth.php
│   └── validation.php
└── en/
    ├── messages.php
    ├── auth.php
    └── validation.php
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
<?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/:

json
// lang/es.json
{
    "Welcome": "Bienvenido",
    "Email": "Correo electrónico",
    "Password": "Contraseña"
}

Usar traducciones

Usa el helper __() o la directiva @lang en Blade:

php
<?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:

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
<?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
<?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
<?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
<?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
<?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
<?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
<?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
<?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:

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
<?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
<?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
<?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
<?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
<?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:

bash
php artisan lang:publish

Esto crea lang/en/validation.php. Copia y traduce para otros idiomas:

php
<?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.php o .env
  • Las traducciones van en lang/ en formato PHP o JSON
  • Usa __() o @lang para 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"

¿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