Cargando…

Esto está tardando más de lo esperado.

Volver al centro de ayuda

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:

  1. Una cuenta con sesión iniciada y un locale en 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.
  2. Un visitante que usó el selector conserva esa elección durante el resto de la visita. Vive en la sesión bajo locale.
  3. Si no, decide el navegador. El encabezado Accept-Language se lee en el orden en que el navegador lista sus idiomas, cada uno reducido a su código de dos letras (es-MX es es), y gana el primero que aparece en app.languages. Un navegador que pide francés y luego inglés recibe inglés, no el idioma de respaldo.
  4. 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}.json guarda las oraciones. La clave es el texto en inglés y el valor su traducción; lang/en.json mapea cada clave a sí misma. Aquí vive casi todo.
  • lang/{codigo}/*.php guarda claves estructuradas con puntos: auth.php, passwords.php, validation.php y pagination.php de Laravel, más legal.php con el texto de términos y privacidad y web.php con 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() y Lang::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 como BelongsTo o HasMany, 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/Enums que usa IsEnum, 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/:

  1. Una clave encontrada en otro idioma y ausente en inglés se agrega al inglés con la clave como valor.
  2. 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.
  3. Un idioma con un archivo lang/{codigo}.json o una carpeta lang/{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

  1. Agrégalo a config/app.php:

    'languages' => [
        'es' => 'Spanish',
        'en' => 'English',
        'fr' => 'French',
    ],
    
  2. Agrega la etiqueta nueva a lang/en.json a mano, como "French": "French". La etiqueta pasa por __() en el selector y en el perfil, pero lang:search no escanea config/, así que no la va a encontrar por ti.

  3. Corre la sincronización. Crea lang/fr.json y lang/fr/*.php con cada clave traducida, o copiada en inglés cuando no hay proveedor configurado:

    php artisan lang:sync
    
  4. 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 lee app.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.