En Laravel 13 no hay un Kernel que editar. El middleware se añade en bootstrap/app.php, y corre en cada petición en el orden en que lo añades, antes de llegar al controlador.
Registramos dos middlewares globales de API para que cada endpoint tenga localización y compresión consistentes:
->withMiddleware(function (Middleware $middleware) {
$middleware->append([
SetRequestLocaleMiddleware::class,
GzipResponseMiddleware::class,
]);
})
Ambas clases vienen del paquete compartido. Ninguna se define en este servicio, y es deliberado: todos los servicios de la flota deberían localizar y comprimir una respuesta de forma idéntica, y la única manera de garantizarlo es no darles oportunidad de diferir.
Esto también afecta a la ruta /health del capítulo 1. Sigue siendo pública (sin autenticación ni límite), pero ahora hereda el comportamiento global: se fija el idioma para etiquetas traducidas y mensajes de validación, y la respuesta puede comprimirse si el cliente acepta gzip y la carga es suficientemente grande.
SetRequestLocaleMiddleware: deteccion de idioma
Deja que quien consume tu API elija su idioma sin librerías especiales ni cableado extra. Este middleware mantiene la localización explícita y predecible.
Los clientes pueden indicar su idioma preferido de dos formas, con esta prioridad:
- Parámetro de consulta (
?locale=es): explícito, máxima prioridad. - Cabecera
Accept-Language: cabecera HTTP estándar, segunda prioridad. - Idioma por defecto: se usa si no hay ninguno de los anteriores.
final readonly class SetRequestLocaleMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$locale = $request->query('locale');
if (! $locale) {
$locale = $this->parseAcceptLanguage($request->header('Accept-Language', ''));
}
/** @var array<string> $supportedLocales */
$supportedLocales = config('webplo.supported_locales', ['en', 'es']);
if (is_string($locale) && in_array($locale, $supportedLocales, true)) {
App::setLocale($locale);
}
return $next($request);
}
}
Accept-Language no es un idioma. Es una lista ordenada de ellos —fr;q=0.9,en-US;q=0.8 significa francés si lo tienes, si no inglés americano— así que comparar la cabecera en crudo contra una lista de códigos soportados falla con cualquier navegador real. Analizarla significa respetar los valores de calidad en orden, y recurrir de una variante a su idioma base:
private function parseAcceptLanguage(string $acceptLanguage): ?string
{
// ... parse each entry into ['locale' => ..., 'quality' => ...],
// discarding any q outside the 0.0-1.0 range RFC 7231 allows.
usort($parsed, fn (array $a, array $b) => $b['quality'] <=> $a['quality']);
foreach ($parsed as $item) {
if (in_array($item['locale'], $supportedLocales, true)) {
return $item['locale'];
}
// 'en-US' is a dialect of a language we do speak.
$baseLocale = explode('-', $item['locale'])[0];
if (in_array($baseLocale, $supportedLocales, true)) {
return $baseLocale;
}
}
return null;
}
Mantén el conjunto soportado en configuración y no en un enum. La lista de idiomas en los que responde una API es un hecho de despliegue, no un hecho de código: un mercado nuevo debería ser un cambio de configuración, no una versión.
La validación contra ese conjunto es crucial: previene ataques de inyección. Un cliente malicioso no puede pedir locale=../../etc/passwd ni locale='; DROP TABLE users;--. Los idiomas inválidos se ignoran en silencio y se usa el predeterminado.
Una vez el middleware fija el idioma con App::setLocale(), todo en Laravel lo usa automáticamente:
// app/Http/Requests/CreateLicenseRequest.php
public function messages(): array
{
return [
'name.required' => Lang::get('validation.name.required'),
'name.string' => Lang::get('validation.name.string'),
'name.max' => Lang::get('validation.name.max'),
'domain.required' => Lang::get('validation.domain.required'),
'domain.string' => Lang::get('validation.domain.string'),
'domain.max' => Lang::get('validation.domain.max'),
];
}
La internacionalización no va sólo de traducir. Va de respeto. Cuando alguien recibe un mensaje de error en su idioma, confía más en tu API. No se siente ajeno. Los ficheros de traducción se cachean tras la primera carga y fijar el idioma ocurre una vez por petición, así que la sobrecarga es despreciable.