laraveli18nphplaravel-13

Typed Translation Accessors en Laravel: Traducciones con Tipado Fuerte

Introducción

Las traducciones son una parte crítica de cualquier aplicación Laravel que busque ser global. Sin embargo, trabajar con archivos de idioma tradicionales puede ser propenso a errores: typos en las claves, valores faltantes en algunos idiomas, o simplemente no saber qué estructura espera tu aplicación.

Laravel 13.15.0 introduce Typed Translation Accessors, una característica que revoluciona cómo gestionamos las traducciones mediante tipado fuerte. En lugar de acceder a traducciones como strings simples, ahora puedes definir tipos específicos que garantizan que tus traducciones tengan la estructura correcta.

En este artículo aprenderás cómo aprovechar esta característica para mejorar la calidad, mantenibilidad y seguridad de tus traducciones.

¿Qué son los Typed Translation Accessors?

Los Typed Translation Accessors permiten definir clases TypeScript-like para tus traducciones que especifican exactamente qué estructura deben tener. Laravel valida automáticamente que tus archivos de idioma coincidan con estos tipos.

El problema sin Typed Accessors

Tradicionalmente, si tienes un archivo de traducción resources/lang/es/messages.php:

return [
    'welcome' => 'Bienvenido',
    'user' => [
        'name' => 'Nombre',
        'email' => 'Correo electrónico',
    ],
];

Accedes a él así:

$message = __('messages.welcome');
$userName = __('messages.user.name');

Los problemas:

  • No hay validación en tiempo de compilación
  • Typos en las claves no se detectan hasta runtime
  • No sabes qué estructura esperan tus vistas
  • El IDE no puede autocompletar

La solución: Typed Translation Accessors

Con esta nueva característica, defines la estructura esperada en una clase de tipo:

// app/Translations/Messages.php
namespace App\Translations;

use Illuminate\Translation\Translator;

class Messages
{
    public function __construct(private Translator $translator) {}

    public function welcome(): string
    {
        return $this->translator->get('messages.welcome');
    }

    public function userName(): string
    {
        return $this->translator->get('messages.user.name');
    }

    public function userEmail(): string
    {
        return $this->translator->get('messages.user.email');
    }
}

Configuración Inicial

Paso 1: Crear la clase de tipos

Crea una carpeta para tus traductores tipados:

mkdir app/Translations

Luego crea una clase para cada grupo de traducciones:

// app/Translations/Auth.php
namespace App\Translations;

use Illuminate\Translation\Translator;

class Auth
{
    public function __construct(private Translator $translator) {}

    public function login(): string
    {
        return $this->translator->get('auth.login');
    }

    public function loginFailed(): string
    {
        return $this->translator->get('auth.login_failed');
    }

    public function resetPassword(): string
    {
        return $this->translator->get('auth.reset_password');
    }
}

Paso 2: Registrar en el Service Provider

En tu app/Providers/AppServiceProvider.php:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Translations\Auth;
use App\Translations\Messages;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->singleton(Auth::class);
        $this->app->singleton(Messages::class);
    }
}

Paso 3: Crear los archivos de idioma

Asegúrate de que tus archivos de idioma existan y tengan las claves correctas:

// resources/lang/es/auth.php
return [
    'login' => 'Iniciar sesión',
    'login_failed' => 'Las credenciales son inválidas',
    'reset_password' => 'Restablecer contraseña',
];

Uso en Controladores

Ahora puedes inyectar tu clase de traducción tipada en controladores:

// app/Http/Controllers/LoginController.php
namespace App\Http\Controllers;

use App\Translations\Auth;
use Illuminate\View\View;

class LoginController extends Controller
{
    public function show(Auth $auth): View
    {
        return view('auth.login', [
            'loginButton' => $auth->login(),
            'resetLink' => $auth->resetPassword(),
        ]);
    }

    public function attempt(Auth $auth)
    {
        // Lógica de autenticación
        if (!$credentials) {
            return back()->withError($auth->loginFailed());
        }
    }
}

Casos de uso avanzados

Traducciones con parámetros

Puedes pasar parámetros a tus traducciones:

// resources/lang/es/messages.php
return [
    'welcome_user' => 'Bienvenido, :name',
    'order_shipped' => 'Tu pedido #:id fue enviado el :date',
];

En tu clase:

// app/Translations/Messages.php
class Messages
{
    public function welcomeUser(string $name): string
    {
        return $this->translator->get('messages.welcome_user', ['name' => $name]);
    }

    public function orderShipped(int $orderId, string $date): string
    {
        return $this->translator->get('messages.order_shipped', [
            'id' => $orderId,
            'date' => $date,
        ]);
    }
}

Uso:

echo $messages->welcomeUser('Juan'); // Bienvenido, Juan
echo $messages->orderShipped(123, '2025-06-11'); // Tu pedido #123 fue enviado el 2025-06-11

Traducciones plurales

Para pluralizaciones:

// resources/lang/es/messages.php
return [
    'apples' => '{0} No hay manzanas|{1} Una manzana|[2,*] :count manzanas',
];

En tu clase:

class Messages
{
    public function apples(int $count): string
    {
        return $this->translator->choice('messages.apples', $count, ['count' => $count]);
    }
}

Uso:

echo $messages->apples(0);  // No hay manzanas
echo $messages->apples(1);  // Una manzana
echo $messages->apples(5);  // 5 manzanas

Estructuras complejas

Para traducciones con estructura anidada:

// resources/lang/es/validation.php
return [
    'required' => 'El campo :attribute es requerido',
    'email' => 'El campo :attribute debe ser un email válido',
    'min' => 'El campo :attribute debe tener al menos :min caracteres',
];

Clase especializada:

// app/Translations/Validation.php
namespace App\Translations;

use Illuminate\Translation\Translator;

class Validation
{
    public function __construct(private Translator $translator) {}

    public function required(string $attribute): string
    {
        return $this->translator->get('validation.required', ['attribute' => $attribute]);
    }

    public function email(string $attribute): string
    {
        return $this->translator->get('validation.email', ['attribute' => $attribute]);
    }

    public function min(string $attribute, int $min): string
    {
        return $this->translator->get('validation.min', [
            'attribute' => $attribute,
            'min' => $min,
        ]);
    }
}

Integración con vistas Blade

Puedes usar tus traducciones tipadas en Blade:

<!-- resources/views/auth/login.blade.php -->
@php
use App\Translations\Auth;
$auth = app(Auth::class);
@endphp

<div class="form-group">
    <label>{{ $auth->login() }}</label>
    <input type="text" name="email" placeholder="Correo">
</div>

<button type="submit">{{ $auth->login() }}</button>

O creando un helper:

// app/helpers.php
function trans(string $class): object
{
    return app($class);
}

Uso en Blade:

<button>{{ trans('App\\Translations\\Auth')->login() }}</button>

Validación automática

Laravel puede validar que tus archivos de idioma coincidan con tus tipos. Crea un comando Artisan:

// app/Console/Commands/ValidateTranslations.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Translation\Translator;
use ReflectionClass;

class ValidateTranslations extends Command
{
    protected $signature = 'translations:validate';
    protected $description = 'Valida que las traducciones coincidan con los tipos';

    public function handle(Translator $translator)
    {
        $translationClasses = [
            \App\Translations\Auth::class,
            \App\Translations\Messages::class,
        ];

        foreach ($translationClasses as $class) {
            $this->validateClass($class, $translator);
        }

        $this->info('✓ Todas las traducciones son válidas');
    }

    private function validateClass(string $class, Translator $translator)
    {
        $reflection = new ReflectionClass($class);
        $methods = $reflection->getMethods();

        foreach ($methods as $method) {
            if ($method->isPublic() && $method->getDeclaringClass()->getName() === $class) {
                // Validar que el método pueda ejecutarse sin errores
                try {
                    $instance = app($class);
                    $method->invoke($instance);
                } catch (\Exception $e) {
                    $this->error("Error en {$class}::{$method->getName()}: " . $e->getMessage());
                }
            }
        }
    }
}

Ejecuta:

php artisan translations:validate

Mejores prácticas

1. Organiza por módulo

app/
  Translations/
    Auth/
      Login.php
      Reset.php
    Dashboard/
      Widget.php
    Common/
      Buttons.php
      Messages.php

2. Documenta con PHPDoc

class Auth
{
    /**
     * Obtiene el mensaje de error de login fallido
     * 
     * Clave: auth.login_failed
     * Idiomas soportados: es, en, fr
     */
    public function loginFailed(): string
    {
        return $this->translator->get('auth.login_failed');
    }
}

3. Usa constantes para claves

class Messages
{
    public const WELCOME = 'messages.welcome';
    public const USER_NAME = 'messages.user.name';

    public function welcome(): string
    {
        return $this->translator->get(self::WELCOME);
    }
}

4. Cachea traducciones en producción

public function welcomeUser(string $name): string
{
    return cache()->remember(
        "translation.messages.welcome_user.{$name}",
        now()->addDay(),
        fn() => $this->translator->get('messages.welcome_user', ['name' => $name])
    );
}

Testing

Prueba tus traducciones:

// tests/Unit/Translations/AuthTest.php
namespace Tests\Unit\Translations;

use App\Translations\Auth;
use Tests\TestCase;

class AuthTest extends TestCase
{
    public function test_login_translation_exists()
    {
        $auth = app(Auth::class);
        $result = $auth->login();

        $this->assertIsString($result);
        $this->assertNotEmpty($result);
    }

    public function test_all_languages_have_translations()
    {
        $languages = ['es', 'en', 'fr'];
        
        foreach ($languages as $lang) {
            app('translator')->setLocale($lang);
            $auth = app(Auth::class);
            
            $this->assertNotEmpty($auth->login());
        }
    }
}

Conclusión

Los Typed Translation Accessors en Laravel 13.15.0 representan un salto significativo en la calidad y mantenibilidad del código de internacionalización. Al proporcionar tipado fuerte y autocomplete del IDE, eliminan toda una clase de errores que antes solo se detectaban en producción.

Esta característica es especialmente valiosa en proyectos grandes con múltiples idiomas, equipos distribuidos y requisitos estrictos de calidad. Implementarla desde el inicio te ahorrará horas de debugging y facilitará la colaboración entre desarrolladores.

Puntos clave

  • Los Typed Translation Accessors permiten definir clases con métodos tipados para acceder a traducciones
  • Proporcionan autocompletado del IDE y validación en tiempo de compilación
  • Se registran como singletons en el Service Provider para inyección de dependencias
  • Soportan parámetros, pluralizaciones y estructuras complejas
  • Pueden ser validados automáticamente mediante comandos Artisan personalizados
  • Mejoran significativamente la mantenibilidad en proyectos multiidioma
  • Se integran perfectamente con Blade y pueden ser testeados como cualquier otra clase
  • Siguen el principio DRY evitando repetición de claves de traducción
  • Son especialmente útiles en aplicaciones SaaS globales y plataformas multiidioma
  • Reducen errores en producción relacionados con traducciones faltantes o incorrectas