Idiomas y traducciones
Cómo se elige el idioma de una página, cómo se escriben y traducen las cadenas, y cómo agregar un idioma.
El kit viene en español e inglés. Un visitante elige idioma con un selector, una cuenta lleva el suyo, y cada cadena que una persona lee se escribe una vez en inglés y se traduce con un comando. Esta guía cubre la configuración, el orden de resolución, los cuatro comandos lang:* y el proveedor de IA detrás de ellos, y los pasos para agregar un tercer idioma.
La configuración
Toda está en config/app.php.
| Clave | Valor | Qué hace |
|---|---|---|
languages |
['es' => 'Spanish', 'en' => 'English'] |
Los idiomas que la aplicación publica. La clave es el código de idioma, que debe coincidir con un archivo lang/{codigo}.json; el valor es la etiqueta que se muestra en el selector y en el perfil, pasada por __() para que ella misma sea traducible. |
fallback_locale |
es |
El idioma cuando nada más decide, y al que Laravel cae cuando una cadena no tiene traducción en el idioma activo. |
locale |
env('APP_LOCALE', 'es') |
El idioma fuera de una petición web: comandos de consola, trabajos en cola y rutas de API. Una petición web nunca lo lee; el middleware de abajo fija el idioma en cada una. |
Cómo se elige el idioma de una página
App\Http\Middleware\LocaleMiddleware corre en cada petición web y se detiene en la primera regla que responde:
- Una cuenta con sesión iniciada y un
localeen su perfil recibe ese idioma en todos sus dispositivos, diga lo que diga el navegador o lo que haya elegido antes de iniciar sesión. - Un visitante que usó el selector conserva esa elección durante el resto de la visita. Vive en la sesión bajo
locale. - Si no, decide el navegador. El encabezado
Accept-Languagese lee en el orden en que el navegador lista sus idiomas, cada uno reducido a su código de dos letras (es-MXeses), y gana el primero que aparece enapp.languages. Un navegador que pide francés y luego inglés recibe inglés, no el idioma de respaldo. - Nada coincidió:
fallback_locale.
Una cuenta recibe su locale al registrarse: App\Observers\UserObserver copia el idioma en que se estaba leyendo la página de registro, así que la primera preferencia se captura sin preguntar. La persona lo cambia después en /account, en la tarjeta "Idioma y hora", desde un select alimentado por app.languages.
El selector de idioma
<x-language-switcher /> dibuja un enlace pequeño por cada idioma de app.languages, resaltando el activo. Se muestra en el encabezado del sitio público, que es la landing y las páginas de características, y en el encabezado del centro de ayuda. No dibuja nada para una cuenta con sesión iniciada, porque esa cuenta lee el idioma de su perfil en todos sus dispositivos y el selector prometería algo que no cumpliría. El layout de la aplicación no tiene selector por la misma razón.
Cada enlace apunta a /locale/{codigo}, una ruta que revisa el código contra app.languages, responde 404 para cualquier otro, guarda el código en la sesión cuando el visitante no tiene sesión iniciada, y redirige de vuelta a la página en la que estaba.
Cómo se escriben las cadenas
Cada cadena que una persona lee pasa por __() con la oración en inglés como clave, tanto en Blade como en PHP:
{{ __('Save changes') }}
{{ __('Welcome back, :name', ['name' => $user->name]) }}
Hay dos tipos de archivo de traducción:
lang/{codigo}.jsonguarda las oraciones. La clave es el texto en inglés y el valor su traducción;lang/en.jsonmapea cada clave a sí misma. Aquí vive casi todo.lang/{codigo}/*.phpguarda claves estructuradas con puntos:auth.php,passwords.php,validation.phpypagination.phpde Laravel, máslegal.phpcon el texto de términos y privacidad yweb.phpcon unos cuantos avisos. Se leen con la clave con puntos,__('legal.terms.title'), y sus arreglos anidados se mantienen sincronizados igual que el JSON.
La regla es que ningún español, ni ningún otro idioma, aparece en el código. Escribe inglés, corre los comandos, y las traducciones siguen.
Los comandos
php artisan lang:search
Escanea app/, resources/views/ y routes/ y agrega cada cadena que encuentra a lang/en.json, con la clave como su propio valor, cuando la clave todavía no está. Reconoce:
__(),@lang(),trans()yLang::get()con una cadena entre comillas simples o dobles.- La etiqueta de un input de administración,
Inputs\Text::make('Label'), excepto en los inputs de relación comoBelongsTooHasMany, cuya etiqueta es el nombre de un modelo. ->setTitle('Title')en inputs y filtros.public $title = 'Label'en acciones de administración.- El nombre en singular y plural de cada recurso de administración en
app/Front/Resources. - La forma en mayúsculas iniciales de cada caso de un enum en
app/Enumsque usaIsEnum, porque ésa es la etiqueta que se muestra para él.
Una clave que contiene un punto, no tiene espacios y está toda en minúsculas se trata como clave con puntos de un archivo PHP y se omite. Córrelo después de agregar texto a la interfaz.
php artisan lang:sync
Hace que cada idioma tenga las mismas claves que el inglés, en el mismo orden, para el archivo JSON y para cada archivo PHP bajo lang/en/:
- Una clave encontrada en otro idioma y ausente en inglés se agrega al inglés con la clave como valor.
- Cada otro idioma se reescribe en el orden del archivo inglés. Una clave que ya tenía conserva su traducción; una clave que le faltaba la traduce el proveedor de IA, o se copia en inglés cuando no hay proveedor configurado.
- Un idioma con un archivo
lang/{codigo}.jsono una carpetalang/{codigo}/se detecta automáticamente, así que se crea un archivo nuevo para un idioma que todavía no tiene ninguno.
Córrelo después de lang:search, y una vez cuando agregues un idioma.
php artisan lang:delete
Quita de cada lang/*.json las claves que nadie usa. Una clave se conserva cuando lang:search la encontraría, o cuando aparece como cadena literal en cualquier archivo .php, .js, .jsx, .ts, .tsx, .vue, .html, .htm, .json, .md, .yaml o .yml del proyecto, vendor/ incluido, fuera de tests/, docs/, node_modules/, storage/, bootstrap/cache/, public/build/ y lang/. Las claves vacías siempre se quitan. Los archivos PHP nunca se tocan, y un archivo sin nada que quitar no se reescribe. Córrelo después de borrar texto de la interfaz.
php artisan lang:update
Corre lang:sync, luego lang:search, luego lang:sync otra vez. Es el único comando que hay que correr después de una tanda de trabajo en la interfaz: recoge las cadenas nuevas y las traduce.
Traducción automática
lang:sync traduce a través de App\Services\LangTranslatorService, que consulta a App\Ai\Agents\TranslationAgent en el proveedor que nombra config('ai.default'). Ése es openai en config/ai.php, con su llave desde .env:
OPENAI_API_KEY=sk-...
Por cada cadena que falta se envía una petición con dos partes: una instrucción que dice traducir texto de una aplicación Laravel al idioma destino, conservar el significado y el tono, preservar los marcadores exactamente y devolver sólo el texto traducido; y la cadena misma, con cada :marcador cambiado por una señal como __PH0__ antes de enviarla y restaurado después, para que el nombre de una variable nunca se traduzca. El modelo es el modelo de texto por omisión del proveedor, gpt-5.4 para OpenAI, a menos que pongas models.text.default bajo el proveedor en config/ai.php.
Cuando OPENAI_API_KEY está vacía el comando no falla: imprime AI translation provider is not configured una vez por cadena, escribe el texto en inglés como valor y sigue. Lo mismo pasa cuando una petición falla o no devuelve nada. Una traducción que volvió en inglés es, por tanto, una cadena para traducir a mano o en la siguiente corrida con una llave.
Cualquier proveedor de config/ai.php funciona cuando cambias ai.default; el driver de Ollama no necesita llave. Cómo configurar uno está en Conecta un proveedor de IA.
Agregar un idioma
-
Agrégalo a
config/app.php:'languages' => [ 'es' => 'Spanish', 'en' => 'English', 'fr' => 'French', ], -
Agrega la etiqueta nueva a
lang/en.jsona mano, como"French": "French". La etiqueta pasa por__()en el selector y en el perfil, perolang:searchno escaneaconfig/, así que no la va a encontrar por ti. -
Corre la sincronización. Crea
lang/fr.jsonylang/fr/*.phpcon cada clave traducida, o copiada en inglés cuando no hay proveedor configurado:php artisan lang:sync -
Traduce el centro de ayuda. Cada guía y cada entrada del changelog espera una copia en
docs/{tu-proyecto}/help/guides/fr/{slug}.md; una guía sin ella se sirve en inglés con un aviso que lo dice. La revisión que encuentra lo que falta también leeapp.languages:php artisan help:stamp --check
Desde ese punto el selector muestra FR, la regla del navegador acepta fr, y el select del perfil ofrece francés.
Cómo se traduce el centro de ayuda
Las guías que estás leyendo son archivos Markdown bajo docs/{tu-proyecto}/help/, con el inglés en el nivel superior y cada traducción en una carpeta con el nombre del idioma, es/ para español. Los comandos lang:* no las tocan: la traducción la escribe una persona o un asistente, y php artisan help:stamp escribe en cada archivo traducido un comentario con una huella que registra de qué inglés se escribió. help:stamp --check lista entonces cada documento sin traducción, uno cuyo inglés cambió después de escribir la traducción, y uno cuyo inglés ya no existe. Dónde van los archivos y cómo los posee un proyecto derivado está en Hazlo tuyo.