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 centro de ayuda es la excepción: sus direcciones llevan su idioma, /help/en/... y /help/es/..., y ese idioma manda ahí para todos. Leer una página de ayuda en otro idioma no cambia nada en el perfil ni en la sesión, así que el resto de la aplicación conserva el idioma que eligieron estas reglas. Mira Escribe guías y el changelog.

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. Fuera 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.

En el centro de ayuda el selector recibe la página actual en cada idioma en que existe, y cada enlace lleva directo a esa página: una guía lleva a la misma guía en el otro idioma. Lo ve cualquier lector, con sesión o sin ella, porque sólo navega y no guarda nada, y no ofrece un idioma en el que la página no está escrita.

El encabezado del sitio público con el selector ES y EN

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 web.php con unos cuantos avisos. Se leen con la clave con puntos, __('web.ios_notice'), y sus arreglos anidados se mantienen sincronizados igual que el JSON. El texto de términos y privacidad vive una carpeta más abajo, en lang/{codigo}/legal/, un archivo por proyecto, y cada idioma lo escribes a mano: mira /help/write-guides-and-changelog.

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(), trans_choice() 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.
  • La forma en mayúsculas iniciales de cada clave de límite declarada en App\Classes\PlanLimits, porque ése es el título que dibujan las tarjetas de consumo. Un límite que agregues después se detecta solo.
  • El nombre y la descripción de cada categoría del centro de ayuda en config/help.php, y el subtítulo debajo de las guías, help.guides_description.
  • El title y el summary de cada característica declarada en resources/views/landing/{tu-proyecto}/features.php, incluso de una cuya página todavía no escribiste. Ésas no las agregas a lang/en.json a mano.

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 junto con los paquetes enlazados ahí, fuera de tests/, docs/, node_modules/, storage/, bootstrap/cache/, public/build/ y lang/. Una clave con salto de línea o comillas también cuenta cuando el código la escribe con escapes como \n o partida en cadenas unidas con .. 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 la única configuración que lang:search lee es la del centro de ayuda, no app.languages, 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 no existe en francés, y su dirección en francés responde 404. 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.