Traducciones en Laravel: locale, middleware, Livewire e Inertia.js paso a paso

- Andrés Cruz - EN In english

Video thumbnail

Laravel incluye soporte nativo para la localización, es decir, la capacidad de adaptar tu aplicación al idioma y la región desde donde se consume. Esto te permite ofrecer dos funcionalidades clave: la detección automática del idioma del usuario y la traducción de textos según su preferencia. En este artículo cubriremos ambos escenarios, desde la configuración básica de app_locale hasta la integración con Livewire e Inertia.js.

Si aún no has visto cómo manejar Excepciones personalizadas en Laravel, te recomiendo echarle un vistazo antes de continuar.

La localización en Laravel es una de esas funciones que, una vez que las dominas, te permiten llevar tus aplicaciones a otro nivel. En mi experiencia, no solo mejora la usabilidad del producto, sino que también abre la puerta a nuevos mercados y usuarios internacionales. A lo largo de esta guía te explicaré cómo configurar, crear y gestionar traducciones en Laravel, apoyándome en ejemplos reales y buenas prácticas.

1. Qué es la localización en Laravel y por qué es importante

La localización (L10n) es el proceso de adaptar tu aplicación a diferentes idiomas, monedas o formatos regionales. Laravel lo integra de forma nativa, facilitando tanto la traducción de textos como la detección automática del idioma del usuario. Es una de las características más potentes del framework y, sorprendentemente, una de las más subestimadas.

En uno de mis primeros proyectos multi-idioma, descubrí que bastaba una configuración mínima para ofrecer la interfaz completa en inglés y español. Esta capacidad es especialmente útil si tu aplicación tiene usuarios internacionales o si planeas expandirte fuera de tu mercado inicial.

Diferencia clave entre los dos conceptos que suelen confundirse:

  • Internacionalización (i18n): preparar la app para manejar múltiples idiomas (estructura, configuración, flujo de datos).
  • Localización (L10n): adaptar los textos y configuraciones específicas a cada idioma o región concreta.

2. Configuración inicial de localización en Laravel

Al crear un proyecto Laravel nuevo, notarás que la carpeta /lang no existe por defecto. En mis primeros intentos pensé que era un error, pero en realidad Laravel espera que la generes tú mismo con un simple comando de Artisan:

$ php artisan lang:publish

Este comando crea la carpeta /lang en la raíz del proyecto y publica los archivos de idioma base que Laravel usa internamente (validaciones, paginación, contraseñas, etc.). Es el punto de partida obligatorio antes de crear tus propios archivos de traducción.

Carpeta /lang generada por lang:publish en Laravel

Dentro de config/app.php puedes definir el idioma por defecto modificando la clave locale:

'locale' => 'es',

Si trabajas con múltiples idiomas, también es recomendable ajustar el fallback_locale (idioma de respaldo):

'fallback_locale' => 'en',

De esta forma, si falta una traducción en español, Laravel mostrará automáticamente el texto en inglés en lugar de devolver la clave en crudo. Esto es especialmente útil durante el desarrollo, cuando los archivos de idioma pueden estar incompletos.

3. Creación de archivos de idioma (PHP y JSON) y cadenas de texto para la traducción

Laravel proporciona dos formatos para gestionar las cadenas de traducción. En ambos casos, los archivos deben ubicarse dentro de la carpeta base /lang:

Formato PHP (archivos organizados por idioma en subcarpetas):

/lang
    /en
        messages.php
    /es
        messages.php

Formato JSON (un archivo por idioma en la raíz de /lang):

/lang
    en.json
    es.json

En esta guía usaremos el formato PHP, ya que es más escalable y permite modularizar los mensajes por contexto (autenticación, mensajes de usuario, validaciones, etc.). Puedes crear tantos archivos como necesites dentro de cada subcarpeta de idioma.

Cada archivo devuelve un array de pares clave/valor, donde la clave identifica el texto y el valor es la traducción correspondiente. Por ejemplo:

lang/en/messages.php

<?php
 
return [
    'welcome' => 'Welcome to our application!',
];

lang/es/messages.php

<?php

return [
   'welcome' => 'Bienvenido a nuestra aplicación!',
];

4. Mostrar traducciones en vistas y controladores

Una vez creadas las cadenas, puedes mostrarlas en cualquier parte de tu aplicación usando la función helper __() o la directiva @lang en Blade:

echo __('messages.welcome')

El formato es archivo.clave: primero el nombre del archivo (sin extensión) y luego la clave dentro del array. La función __() devuelve el texto traducido correspondiente al locale activo; si la clave no existe, devuelve la propia clave como texto de respaldo. Este esquema funciona tanto en controladores PHP como en vistas Blade.

También puedes usar la directiva de Blade @lang('messages.welcome'), que es equivalente y resulta especialmente legible dentro de las plantillas. Ambas opciones son válidas; la elección es cuestión de preferencia.

Errores comunes que debes evitar:

  • Usar una clave incorrecta (Laravel devolverá la clave literal en lugar de la traducción).
  • No haber ejecutado php artisan lang:publish para generar la carpeta /lang.
  • Olvidar limpiar la caché con php artisan config:clear después de cambiar archivos de idioma o configuración.

5. Cómo cambiar el idioma dinámicamente en tu app

Para cambiar el idioma durante la navegación, puedes hacerlo manualmente dentro de un controlador o, de forma más elegante, mediante un middleware de localización que intercepte cada petición. El middleware es la opción recomendada porque centraliza la lógica y la mantiene fuera de tus controladores.

Genera el middleware con Artisan:

$ php artisan make:middleware Localization

Y dentro del método handle estableces el locale según el parámetro recibido en la petición:

public function handle($request, Closure $next)
{
   $locale = $request->get('lang', config('app.locale'));
   app()->setLocale($locale);
   return $next($request);
}

Así, si el usuario visita /home?lang=es, la aplicación cambiará automáticamente al español para esa petición.

En uno de mis proyectos, añadí un selector de idioma en la cabecera del sitio y guardé la preferencia en sesión para mantener la elección del usuario entre visitas:

session(['locale' => $locale]);
app()->setLocale(session('locale', 'es'));

6. Localización avanzada y buenas prácticas

Una vez que dominas la configuración básica, puedes avanzar hacia escenarios más complejos:

  • Traducción de contenido dinámico (desde la base de datos): usando paquetes como Spatie Translatable, que almacena las traducciones directamente en columnas JSON sin necesidad de tablas adicionales por idioma.
  • Rutas traducidas: con paquetes como mcamara/laravel-localization puedes tener URLs amigables por idioma (p. ej., /es/blog vs. /en/blog).
  • SEO multi-idioma: implementando etiquetas <link rel="alternate" hreflang="..."> para indicar a los buscadores las versiones de la página en cada idioma.

Además, Laravel permite pluralización y sustitución de variables en las cadenas de traducción directamente desde los archivos de idioma:

'notifications' => '{0} No tienes notificaciones|{1} Tienes una notificación|[2,*] Tienes :count notificaciones',

Con esta estructura, el framework elige automáticamente la frase correcta según el número proporcionado. Para usarla, llamas a trans_choice('messages.notifications', $count) en lugar de la función habitual __(). También puedes incluir variables de reemplazo en cualquier cadena: por ejemplo, 'greeting' => '¡Hola, :name!' se resuelve llamando a __('messages.greeting', ['name' => 'Andrés']).

Middleware para verificar el prefijo de idioma es/en en Laravel

Video thumbnail

El siguiente desarrollo consiste en crear un middleware que detecte el idioma configurado en la URL mediante un prefijo como es o en y establezca el locale de la aplicación en consecuencia, usando las cadenas de traducción que definimos anteriormente. Además, si el prefijo no es válido, se redireccionará automáticamente al idioma por defecto. Para crearlo:

$ php artisan make:middleware LanguagePrefixMiddleware

Con el siguiente contenido:

app\Http\Middleware\LanguagePrefixMiddleware.php

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class LanguagePrefixMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next): Response
    {

        $language = $request->segment(1);
        
        if(!in_array($language,['es','en'])){
            return redirect('/es/blog');
        }

        app()->setLocale($language);

        return $next($request);
    }
}

El middleware es directo: obtiene el primer segmento de la URL con $request->segment(1) y verifica si es un locale válido (es o en). Si no lo es, redirige a /es/blog como fallback. En caso contrario, establece el locale con app()->setLocale($language) y continúa con la petición normalmente.

Registramos el middleware en las rutas de la siguiente forma:

Route::get('/{lang}/mi-ruta', 'MiControlador@miMetodo');

En nuestra aplicación agrupamos las rutas del blog con una función auxiliar y aplicamos el middleware a los dos grupos de rutas (con y sin prefijo de locale):

function routeBlog() {
    Route::get('', [BlogController::class, 'index'])->name('blog.index');
    Route::get('detail/{id}', [BlogController::class, 'show'])->name('blog.show');
}

Route::group(['prefix' => '{locale}/blog','middleware' => LanguagePrefixMiddleware::class], function () {
    routeBlog();
});

Route::group(['prefix' => 'blog','middleware' => LanguagePrefixMiddleware::class], function () {
    routeBlog();
});

Creamos la función auxiliar routeBlog() para centralizar la definición de rutas del blog y reutilizarla en ambos grupos. Así, el blog es accesible con el locale explícito en la URL:

  • es/blog/*
  • en/blog/*

Y cuando se accede sin el prefijo de locale, el middleware intercepta la petición y redirige automáticamente al español:

  • blog/* → redirige a /es/blog/*

Traducciones en Laravel con Inertia.js

Video thumbnail

Vamos a ver cómo crear una aplicación multi-idioma tal como lo haríamos en un desarrollo Laravel tradicional, pero adaptado al contexto de Inertia.js con Vue. Cabe destacar que esta misma implementación funciona de forma idéntica si en tu arquitectura utilizas React o Svelte en lugar de Vue.

En Laravel "puro", acceder a una traducción es tan simple como:

__('messages.welcome')

O también:

trans('messages.welcome')

Recordemos que en el esquema nativo de Laravel creamos los archivos de traducción en /lang y accedemos a los textos mediante las funciones helper __() o trans(). El problema radica en que, al trabajar con Inertia.js, ya no estamos en archivos Blade, sino en componentes .vue, .jsx o .svelte. Al cambiar de entorno, perdemos el acceso directo a esas funciones de servidor, ya que se ejecutan en PHP y no están disponibles en JavaScript.

La solución manual

Anteriormente, la forma habitual de abordar esto era creando un middleware personalizado que leía los archivos de traducción en el backend y los inyectaba de forma global en el cliente mediante Inertia::share().

Si bien este enfoque funciona (es muy similar a lo que hacemos con los mensajes flash en Inertia), la realidad es que resulta bastante engorroso. Pasar manualmente colecciones masivas de traducciones de PHP a JavaScript puede sobrecargar el tamaño de la página inicial y ensuciar la lógica de negocio con responsabilidades que no le corresponden al middleware.

La solución elegante: integración bilateral con un paquete

Para evitarnos ese trabajo manual, vamos a usar un paquete especializado que conecta el mundo de Laravel con el ecosistema de frontend (Vue/React/Svelte) de forma completamente transparente: erag/laravel-lang-sync-inertia.

La implementación requiere dos instalaciones: una en el servidor (PHP) y otra en el cliente (Node):

Instalación en el backend (PHP)

Ejecutamos el comando de Composer para instalar el paquete en nuestro núcleo de Laravel:

$ composer require erag/laravel-lang-sync-inertia

Generamos la carpeta de lenguaje:

$ php artisan lang:publish

Publicamos el archivo de configuración del paquete:

$ php artisan erag:install-lang

E instalamos el paquete de Node, que es el que consumimos desde los componentes Vue:

$ npm install @erag/lang-sync-inertia

Configuración de los archivos de idioma (Lang)

El primer paso es tener listos nuestros archivos de traducción en el backend. Esto se hace de la forma tradicional de Laravel: creamos la carpeta lang/ en la raíz del proyecto (o dentro de resources/lang/ según tu versión de Laravel) y generamos las subcarpetas para cada idioma, como es/ o en/, con sus respectivos archivos PHP que retornan arrays.

Como este procedimiento ya lo vimos a detalle anteriormente, lo daré por sentado; recuerda que es el estándar del framework para indexar cadenas de texto. A modo de ejemplo completo, así quedarían los archivos para esta demo:

lang\es\messages.php

<?php

return [
    'title' => 'Demo de Localización',
    'welcome' => '¡Bienvenido a nuestra aplicación!',
    'description' => 'Esta es una demostración de localización con Laravel Inertia.',
    'greeting' => '¡Hola, :name!',
    'select_language' => 'Seleccionar Idioma',
    'current_language' => 'Idioma Actual',
    'switch_to' => 'Cambiar a',
    'content' => [
        'intro' => 'Bienvenido a la página de demostración de localización.',
        'features' => 'Características',
        'feature_1' => 'Fácil gestión de traducciones',
        'feature_2' => 'Sincronización automática con el frontend',
        'feature_3' => 'Soporte para múltiples idiomas',
        'footer' => '¡Gracias por visitarnos!',
    ],
    'buttons' => [
        'submit' => 'Enviar',
        'cancel' => 'Cancelar',
        'save' => 'Guardar',
        'back' => 'Volver',
    ],
];

lang\en\messages.php

<?php

return [
    'title' => 'Localization Demo',
    'welcome' => 'Welcome to our application!',
    'description' => 'This is a demonstration of Laravel Inertia localization.',
    'greeting' => 'Hello, :name!',
    'select_language' => 'Select Language',
    'current_language' => 'Current Language',
    'switch_to' => 'Switch to',
    'content' => [
        'intro' => 'Welcome to the localization demo page.',
        'features' => 'Features',
        'feature_1' => 'Easy translation management',
        'feature_2' => 'Automatic sync with frontend',
        'feature_3' => 'Support for multiple languages',
        'footer' => 'Thank you for visiting!',
    ],
    'buttons' => [
        'submit' => 'Submit',
        'cancel' => 'Cancel',
        'save' => 'Save',
        'back' => 'Go Back',
    ],
];

Lógica en las rutas y el controlador

Para controlar el cambio de idioma dinámico, preparé dos rutas en routes/web.php: una encargada de renderizar la vista principal (index) y otra diseñada específicamente para procesar la acción de cambiar la localización (changeLanguage).

El controlador de idiomas

Veamos cómo gestionamos la petición del cliente en el controlador:

app\Http\Controllers\LocalizationController.php

<?php

namespace App\Http\Controllers;

use Inertia\Inertia;

class LocalizationController extends Controller
{
    public function index()
    {
        $locale = session('locale', 'en');
        app()->setLocale($locale);

        syncLangFiles('messages');

        return Inertia::render('localization/Index');
    }

    public function changeLanguage(string $locale)
    {
        $availableLocales = ['en', 'es'];

        if (! in_array($locale, $availableLocales)) {
            $locale = 'en';
        }

        session(['locale' => $locale]);
        app()->setLocale($locale);

        return to_route('localization.index');
    }
}

routes\web.php

// LOCALIZATION
Route::prefix('localization')->group(function () {
    Route::get('/', [LocalizationController::class, 'index'])->name('localization.index');
    Route::get('/lang/{locale}', [LocalizationController::class, 'changeLanguage']);
});

Consumo de traducciones en el componente Vue

Una vez que el paquete está instalado e inyectado en la instancia de Vue, el uso en los componentes es sumamente limpio. Disponemos de las dos sintaxis clásicas mediante el composable vueLang(), que expone tanto __() como trans(); puedes elegir la que más se adapte a tu estilo:

resources\js\pages\localization\Index.vue

<script setup>
import { ref } from 'vue';
import { router } from '@inertiajs/vue3';
import { vueLang } from '@erag/lang-sync-inertia';

const { trans, __ } = vueLang();

const currentLocale = ref('en');

const availableLocales = [
    { code: 'en', name: 'English', flag: '' },
    { code: 'es', name: 'Español', flag: '' },
];

function changeLocale(locale) {
    currentLocale.value = locale;
    router.visit(`/localization/lang/${locale}`, {
        preserveState: true,
    });
}
</script>

<template>
    <div class="min-h-screen p-8">
        <div class="mx-auto max-w-4xl">
            <div class="mb-8">
                <h1 class="mb-2 text-3xl font-bold text-gray-800">
                    {{ __('messages.title') }}
                </h1>
                <p class="text-gray-600">
                    {{ __('messages.description') }}
                </p>
            </div>

            <div class="mb-8 rounded-lg p-6 shadow">
                <h2 class="mb-4 text-lg font-semibold">
                    {{ __('messages.current_language') }}
                </h2>
                <div class="flex gap-2">
                    <button
                        v-for="locale in availableLocales"
                        :key="locale.code"
                        @click="changeLocale(locale.code)"
                        class="rounded px-4 py-2 transition-colors"
                        :class="currentLocale === locale.code ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'"
                    >
                        {{ locale.flag }} {{ locale.name }}
                    </button>
                </div>
            </div>

            <div class="rounded-lg bg-white p-6 shadow">
                <h2 class="mb-4 text-xl font-bold text-gray-800">
                    {{ trans('messages.welcome') }}
                </h2>
                <p class="mb-4 text-gray-600">
                    {{ __('messages.content.intro') }}
                </p>
                <div class="mb-6">
                    <h3 class="mb-3 text-lg font-semibold text-gray-700">
                        {{ __('messages.content.features') }}
                    </h3>
                    <ul class="space-y-2">
                        <li class="flex items-center gap-2">
                            <span class="text-green-500">✓</span>
                            {{ __('messages.content.feature_1') }}
                        </li>
                        <li class="flex items-center gap-2">
                            <span class="text-green-500">✓</span>
                            {{ __('messages.content.feature_2') }}
                        </li>
                        <li class="flex items-center gap-2">
                            <span class="text-green-500">✓</span>
                            {{ __('messages.content.feature_3') }}
                        </li>
                    </ul>
                </div>
                <div class="mb-6">
                    <h3 class="mb-3 text-lg font-semibold text-gray-700">
                        {{ trans('messages.greeting', { name: 'Developer' }) }}
                    </h3>
                </div>
                <div class="flex gap-4">
                    <button class="rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600">
                        {{ __('messages.buttons.submit') }}
                    </button>
                    <button class="rounded bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300">
                        {{ __('messages.buttons.cancel') }}
                    </button>
                    <button class="rounded bg-green-500 px-4 py-2 text-white hover:bg-green-600">
                        {{ __('messages.buttons.save') }}
                    </button>
                </div>
                <div class="mt-8 border-t pt-4">
                    <p class="text-gray-500">
                        {{ __('messages.content.footer') }}
                    </p>
                </div>
            </div>

        </div>
    </div>
</template>

Lo más importante de la implementación anterior es la función syncLangFiles, que es la encargada de sincronizar los archivos de traducción de PHP hacia el frontend en cada petición:

syncLangFiles('messages');

Puedes sincronizar varios archivos a la vez pasando un array:

syncLangFiles(['messages', 'auth']);

Y si necesitas generar el JSON de las traducciones para el frontend (útil en builds de producción o para pre-compilar los archivos de idioma):

$ php artisan erag:generate-lang

Seleccionar idioma en Laravel con Livewire

Video thumbnail

Te muestro cómo puedes manejar el cambio de idioma directamente en Laravel con Livewire. En el video puedes ver que la interfaz cambia al español y de vuelta al inglés en tiempo real, sin recargar la página manualmente.

La implementación es sencilla: utilizaremos Livewire para gestionar la preferencia de idioma del usuario. Si por alguna razón prefieres no usar Livewire, con los conocimientos básicos de Laravel puedes adaptar esta lógica a cualquier otro enfoque. Lo verdaderamente importante es entender qué funciones hay que llamar y en qué momento, independientemente de cómo lo implementes.

El componente y el manejo del locale

Este es el componente Livewire. Cada vez que el usuario cambia el idioma mediante wire:model.live, Livewire invoca automáticamente el método render(), donde gestionamos el cambio de locale:

<?php

namespace App\Livewire\User;

use Livewire\Attributes\Layout;
use Livewire\Component;

use Illuminate\Support\Facades\App;

#[Layout('layouts.store')]
class UserProfile extends Component
{

    public $language;

    public function render()
    {
        if (!isset($this->language)) {
            // Al cargar el componente por primera vez, tomamos el locale de la sesión
            // o el que tenga configurado la aplicación en ese momento.
            $this->language = session('locale') ?? App::getLocale();
        } else {
            // Al cambiar el selector (wire:model.live), guardamos en sesión y aplicamos el locale.
            session(['locale' => $this->language]);
            App::setLocale($this->language);
        }
        
        return view('livewire.user.user-profile');
    }
}

Hay muchas formas de implementarlo; esta fue la que mejor me funcionó en la práctica. Lo importante es que el locale llegue correctamente al componente. El flujo es el siguiente:

  • Si $language no está definido, inicializamos el locale con el valor almacenado en sesión (usando session('locale')) o, si no hay nada en sesión, con el locale actual de la aplicación mediante App::getLocale().
  • Cuando el usuario cambia el selector, wire:model.live dispara el re-render y entramos en el bloque else, donde guardamos la preferencia en sesión con session(['locale' => $this->language]).
  • Es crítico llamar a App::setLocale($this->language) para aplicar el cambio a nivel del framework en esa misma petición; sin esto, las traducciones no se actualizarán aunque el valor esté correctamente guardado en sesión.

Aquí también puedes aprovechar para persistir la preferencia del usuario en base de datos. En otro artículo comenté cómo guardar preferencias sin necesidad de crear 20 columnas: la clave es almacenar un pequeño objeto JSON en una sola columna. Es exactamente el patrón que podemos aplicar aquí.

Transición y recarga de interfaz

En Livewire, para que el cambio de idioma se refleje en toda la interfaz (no solo en el componente), añadí un pequeño fragmento de Alpine.js que espera unos milisegundos a que la petición de wire:model se complete y luego recarga la página completa:

<flux:select :label="__('Language')" id="language" wire:model.live="language" x-data @change="setTimeout(function(){window.location.reload()}, 100)" class="mt-1 block w-full rounded border-gray-300">
    <option value="es">Español</option>
    <option value="en">English</option>
</flux:select>

El atributo x-data de Alpine.js inicializa el scope reactivo del elemento, mientras que @change escucha el evento de cambio del selector. El pequeño delay de 100ms garantiza que la petición de Livewire ya se haya completado antes de ejecutar el window.location.reload(), evitando condiciones de carrera entre la respuesta del servidor y el refresco de la página.

Middleware para mantener el idioma entre peticiones

Es fundamental que el idioma seleccionado se mantenga en todas las peticiones posteriores. Sin un middleware, al navegar a otra sección de la app (por ejemplo, al blog), el locale volvería al valor por defecto configurado en config/app.php.

Para resolverlo, implementamos un middleware que lee el locale almacenado en sesión y lo aplica en cada petición entrante:

$ php artisan make:middleware SetLocale

Dentro de ese middleware, establecemos el locale desde la sesión únicamente si existe un valor guardado. También puedes usar el segundo parámetro de session() para definir el idioma por defecto directamente, pero en mi caso preferí usar el condicional explícito para mayor claridad:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\App;

class SetLocale
{
    public function handle($request, Closure $next)
    {

        if (session('locale')) {
            App::setLocale(session('locale'));
        }
        
        return $next($request);
    }
}

¿Dónde registrar el middleware?
Puedes aplicarlo directamente a grupos de rutas específicos:

Route::middleware([SetLocale::class])

Pero la forma más limpia desde Laravel 11 en adelante es registrarlo en el grupo web dentro de bootstrap/app.php, para que se ejecute automáticamente en cada petición HTTP:

bootstrap/app.php

->withMiddleware(function (Middleware $middleware) {
    $middleware->web([
        // Tu middleware personalizado
            \App\Http\Middleware\SetLocale::class,
    ]);
})

Recuerda que los middleware se ejecutan antes de que el controlador procese la petición, por lo que es el lugar ideal para establecer configuraciones globales como el locale. Registrarlo en el grupo web garantiza que aplique a todas las rutas de tu aplicación sin necesidad de añadirlo manualmente en cada definición de ruta.

Archivos de idioma en formato JSON

Crea tus archivos de traducción para es e en en formato JSON. La ventaja de este formato es que puedes usar la cadena literal como clave, lo que resulta muy práctico para textos cortos y reutilizables entre vistas sin necesidad de inventarte nombres de clave descriptivos:

resources/lang/es.json

{
    "Light": "Claro",
    "Dark": "Oscuro",
     ***   
}   

resources/lang/en.json

{
    "Light": "Light",
    "Dark": "Dark",
     ***   
}    

Luego, desde las vistas Blade, referencias el texto usando __() con el nombre del campo como clave:
Por ejemplo:

{{ __('Light') }}

De esta forma tan directa, tenemos configurado el sistema de idiomas en Laravel con Livewire. Todo se reduce a dos pasos clave: guardar la preferencia del usuario (en sesión o base de datos) y aplicar el locale en cada petición mediante un middleware.

7. Conclusión: experiencia y recomendaciones prácticas

La localización es una de esas funcionalidades que marca la diferencia entre una app funcional y una app preparada para escalar globalmente.
En mi experiencia, lo más importante es mantener una estructura clara en los archivos de idioma, usar nombres de claves coherentes y documentar las nuevas traducciones en equipo para evitar duplicidades.

Si trabajas en equipo, un consejo adicional es usar herramientas como Phrase o Linguise para sincronizar traducciones entre desarrolladores y evitar inconsistencias.

Y recuerda: aunque Laravel hace gran parte del trabajo pesado por ti, la calidad de la localización depende de lo organizado que mantengas tus textos y tu flujo de trabajo. Un sistema de traducciones bien estructurado desde el inicio te ahorrará muchos dolores de cabeza cuando la app crezca.

 

❓ Preguntas frecuentes

  • ¿Qué diferencia hay entre traducciones JSON y PHP en Laravel?
    • Los archivos PHP permiten agrupar textos por módulo mediante arrays anidados, lo que los hace más escalables en proyectos grandes. Los archivos JSON, en cambio, usan la cadena literal como clave, lo que es más práctico para textos simples y reutilizables entre vistas.
  • ¿Cómo detecto el idioma del usuario automáticamente?
    • Puedes usar $request->getPreferredLanguage() para leer el header Accept-Language del navegador, o crear un middleware que detecte el idioma a partir del prefijo de la URL, como vimos en esta guía.
  • ¿Puedo traducir contenido dinámico almacenado en base de datos?
    • Sí, con el paquete Spatie Translatable puedes guardar versiones en varios idiomas directamente en columnas JSON de tu base de datos, sin necesidad de tablas adicionales.
  • ¿Cómo cambio el idioma sin recargar la página completa?
    • Con Livewire puedes usar wire:model.live para actualizar el locale en el servidor sin recargar la página. Si necesitas que el cambio afecte a toda la interfaz, combínalo con un pequeño script de Alpine.js que refresque la página tras la petición, como mostramos en esta guía.

El siguiente paso es aprender el sistema de Autorización en Laravel con Gates y Policies.

Aprende a configurar el sistema de traducciones en Laravel: publica los archivos de idioma con lang:publish, ajusta el app_locale y el fallback_locale, crea un middleware de traducción y gestiona el cambio de idioma en español e inglés con Livewire y con Inertia.js.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.