> ## Documentation Index
> Fetch the complete documentation index at: https://botmesh.afaya.com.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Internacionalización (i18n)

> @botmesh/core trae una capa de i18n pequeña: registra un árbol de mensajes por idioma con bot.locale() y traduce con ctx.t() en los handlers y con c.t() en las conversaciones. El idioma se resuelve en cada mensaje, así que cada usuario recibe las respuestas en el suyo.

```ts theme={null}
bot.locale('es', {
  greeting: '¡Hola, {name}!',
  menu: { title: 'Menú principal' },
  cart: { zero: 'Carrito vacío', one: '{count} producto', other: '{count} productos' },
})
bot.locale('en', {
  greeting: 'Hello, {name}!',
  menu: { title: 'Main menu' },
  cart: { one: '{count} item', other: '{count} items' },
})
bot.locale({ defaultLocale: 'es' })

bot.when('hola', (ctx) => ctx.reply(ctx.t('greeting', { name: ctx.senderInfo?.displayName ?? '' })))
bot.when('carrito', (ctx) => ctx.reply(ctx.t('cart', { count: 3 })))
```

## Mensajes

* Las claves se **anidan** (`ctx.t('menu.title')`); una clave también puede
  llevar puntos literales (`{ 'menu.title': '…' }`). Registrar otra vez el
  mismo idioma hace merge profundo.
* Los placeholders `{param}` se interpolan; los desconocidos se dejan tal cual.
* Una hoja `{ one, other, zero?, two?, few?, many? }` es una **forma plural**
  que se elige con `params.count` e `Intl.PluralRules`. `zero` también aplica
  a un `count === 0` exacto en idiomas sin categoría cero.
* Una clave que falta cae al idioma base (`pt-BR` → `pt`), luego al idioma
  por defecto y por último a la propia clave (con un log de nivel debug).

## Cómo se resuelve el idioma

Dentro de la cola por chat, antes de los hooks `before('message')` (ver
[el pipeline de entrada](https://botmesh.afaya.com.co/es/guide/concepts.md#el-pipeline-de-entrada)), `ctx.lang`
toma el primer candidato que sirva un **idioma registrado**:

1. La preferencia guardada del usuario: la clave `'lang'` de `ctx.user` (la escribe `ctx.setLang()`; cambia la clave con `userKey`).
2. El idioma que el provider informa del remitente: `SenderInfo.language`
   o `platformData.languageCode` / `language` / `locale` (p. ej. el
   `language_code` de Telegram). Se desactiva con `providerLanguage: false`.
   Una actualización que no informa idioma (el `language_code` de Telegram
   es opcional) reutiliza el último informado para el mismo remitente,
   guardado en memoria, para que el chat no salte al idioma por defecto entre
   turnos.
3. Tu función `detect(ctx)`, si la configuras.
4. `defaultLocale` (por defecto `'en'`, o `BotConfig.lang`).

`ctx.langSource` dice qué paso ganó (`'user'`, `'provider'`, `'detector'`
o `'default'`; `undefined` con el i18n apagado). Si un hook
`before('message')` reescribe `ctx.body` (por ejemplo, la transcripción de
una nota de voz de `@botmesh/voice`), hay un `detect` configurado y el idioma
salió de él o del valor por defecto, se vuelve a resolver después de los hooks para que
`detect` vea el texto hablado. Se respeta un `ctx.lang` que el hook haya
puesto a mano.

`ctx.defaultLang` es el `defaultLocale` del bot, sea cual sea el `ctx.lang`
resuelto: los plugins que traen textos solo para algunos idiomas (como
`@botmesh/ai`) lo usan de reserva para un idioma que no cubren.

La resolución se activa con cualquier llamada a `bot.locale()` o con
`BotConfig.i18n`. Sin ninguna de las dos, el bot responde en su idioma por
defecto (mensajes internos en inglés, o en el idioma de `lang`) y no lee nada
del store.

### Un bot en un solo idioma: `lang`

Los textos internos están en inglés por defecto. `lang` elige otro idioma
incluido para ellos (y para `ctx.lang`) **sin** activar la resolución: no lee
`ctx.user`, no usa el idioma del provider ni un detector. Es todo lo que
necesita un bot escrito en un solo idioma:

```ts theme={null}
const bot = new Bot({ provider, lang: 'es' })   // textos internos en español
```

Si pones `lang` e `i18n.defaultLocale` a la vez, deben coincidir: la misma
etiqueta, o el mismo idioma con una etiqueta más específica (`'es'` y `'es-MX'`,
y la más específica queda como predeterminada). Idiomas o regiones distintos
lanzan `ConfigValidationError`. Usa `i18n` cuando el bot atienda varios
idiomas.

```ts theme={null}
const bot = new Bot({
  provider,
  i18n: { defaultLocale: 'es', detect: (ctx) => (/^\s*hello\b/i.test(ctx.body ?? '') ? 'en' : undefined) },
})
```

### Fijar el idioma del bot

El idioma que informa el provider es el del **cliente** del usuario, no
necesariamente el de tu bot. Si tu contenido (persona, flujos, títulos de
botones) existe en un solo idioma, un cliente en otro idioma recibiría los
textos internos ("Voy a: … ¿Confirmas?", respuestas de los comandos slash,
timeouts) en su idioma alrededor de contenido en el tuyo, y un usuario con
dos clientes en idiomas distintos podría verlos cambiar de un turno a otro.
Fija los idiomas que sirve el bot con `locales`:

```ts theme={null}
const bot = new Bot({ provider, i18n: { defaultLocale: 'es', locales: ['es'] } })
```

Una preferencia guardada, un idioma del provider o uno detectado que no esté
en `locales` se ignora y la resolución cae a `defaultLocale` (que siempre se
sirve); `ctx.setLang()` lo rechaza. Las variantes regionales siguen a su base
(`'es'` sirve `'es-MX'`). En resumen, la precedencia: **los idiomas que sirve
el bot (`locales`) acotan todo; dentro de ellos, la elección explícita del
usuario → el idioma de su cliente → `detect` → `defaultLocale`.** Sin
`locales` se sirve cualquier idioma registrado (incluidos los `es` y `en`
internos). Para un bot bilingüe, lista ambos (`locales: ['en', 'es']`) y
traduce también tu contenido, por ejemplo construyendo la persona según
`ctx.lang`.

## Cambiar de idioma

```ts theme={null}
bot.command('lang', async (ctx, [code]) => {
  if (!code || !bot.locales().includes(code.toLowerCase())) return ctx.reply(bot.locales().join(', '))
  await ctx.setLang(code)               // se guarda en ctx.user; ctx.lang cambia en el acto
  await ctx.reply(ctx.t('menu.title'))
})
```

`ctx.setLang(code)` guarda el idioma registrado que sirve a `code`
(`'en-GB'` → `'en'` si solo existe `en`). Rechaza con `TypeError` si el
código está vacío o ningún idioma lo sirve, y con `Error` si el i18n está
desactivado.

## Conversaciones

`c.t()` y `c.lang` siguen el **último** mensaje de la conversación: un cambio
de idioma a mitad de conversación se aplica al siguiente paso y también al
destino de `c.goto()`:

```ts theme={null}
bot.conversation('menu', async (c) => {
  await c.say(c.t('menu.title'))
})
bot.use(flow('menu').say((_ctx, c) => c.t('menu.title')))
```

## Fuera de los handlers

```ts theme={null}
const lang = await bot.langOf(userId)          // preferencia guardada o el idioma por defecto
await bot.send(chatId, bot.t('greeting', { name: 'Ana' }, lang))
bot.locales()                                  // ['es', 'en', ...]
```

## Mensajes internos

Los mensajes propios del core (timeout, reintentos y error de conversación,
validación de esperas —incluido el rechazo de un `schema` sin mensaje
propio—, respuestas de `slashCommandsPlugin` y las preguntas, pistas,
palabra para saltar y errores de validación de `flow().form()` bajo
`core.form.*`) viven bajo `core.*` con valores por defecto en `es` y `en`. Sus claves se exportan como
`CORE_MESSAGES`. Para cambiarlos, o traducirlos a un idioma nuevo, usa
`bot.locale()`:

```ts theme={null}
bot.locale('es', { core: { conversation: { timeout: '⏰ Se acabó el tiempo.' } } })
bot.locale('pt', { core: { commands: { ping: 'pong 🏓' } } })
bot.locale('es', { core: { form: { skipWord: 'ninguno', min: '❌ {label} debe ser al menos {min}.' } } })
```

Un `conversation.timeoutMessage` / `retriesMessage` / `errorMessage`
configurado en `BotConfig` sigue ganando al idioma.

`@botmesh/ai` hace lo mismo bajo `ai.*` (claves exportadas como `AI_MESSAGES`,
valores por defecto `es` y `en` en `AI_LOCALES`, ambos congelados): las respuestas de reserva del
autoresponder cuando el proveedor falla o no contesta nada (`ai.error`) o se niega
(`ai.refusal`), salvo que fijes `errorMessage` / `refusalMessage`, la pregunta de confirmación
de herramientas (`ai.tools.confirm`, `{tool}` / `{args}`; `ai.tools.confirmButtons`
y las etiquetas `ai.tools.button.yes` / `ai.tools.button.no` en canales con
botones, y los avisos `ai.tools.confirmAnswered` / `ai.tools.confirmExpired`
donde los botones no se pueden quitar) y sus piezas
(`ai.tools.confirmRepeat`, `ai.tools.ago.*`, `ai.tools.value.*`,
`ai.tools.more`), las notas del prompt de sistema (`ai.tools.confirmNote`,
`ai.tools.doneNote`), la respuesta al
agotar `maxSteps` (`ai.tools.maxSteps`), la respuesta cuando las tool calls
del último paso se cortaron por el tope de tokens (`ai.tools.truncated`) y la línea de `appendSources`
(`ai.rag.sources`, `{sources}`). Una clave que tus idiomas no definen cae al
texto interno en `ctx.lang`; para un idioma sin textos internos (cualquiera
salvo `es` y `en`), en el idioma por defecto del bot (`ctx.defaultLang`) si los
tiene, y si no, en inglés:

```ts theme={null}
bot.locale('es', { ai: { rag: { sources: '📚 {sources}' } } })
```

## Siguientes pasos

* [Referencia de configuración](https://botmesh.afaya.com.co/es/guide/configuration.md): `lang`, `i18n` y las demás opciones de `new Bot()`
* [Datos por usuario](https://botmesh.afaya.com.co/es/guide/user-data.md): dónde guarda `ctx.setLang()` la elección
* [Plugin de IA](https://botmesh.afaya.com.co/es/guide/ai.md): respuestas en el idioma del usuario


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.