Por qué existen los builders localizados
Cuando defines componentes con decorators como @Button, @StringSelect o @ModalComponent, el texto visible suele vivir en metadatos del decorator: labels, placeholders, descripciones de opción, títulos y nombres de campos. En un bot de un solo idioma eso basta. En un bot localizado, eso se vuelve repetitivo muy rápido, porque de otro modo cada handler tendría que resolver manualmente las mismas strings antes de enviar el componente.
Los builders localizados eliminan esa repetición. Leen los metadatos i18n declarados en la clase del componente, resuelven cada key para el locale solicitado mediante I18nService y devuelven un builder de discord.js listo para enviar. El handler se mantiene centrado en el flujo de interacción en lugar de en la plomería de traducción.
El flujo habitual es simple:
- Declara texto de fallback y keys i18n en el decorator del componente.
- Resuelve el locale objetivo en el handler.
- Llama al builder localizado correspondiente.
- Envía la fila, modal, embed o payload V2 que se devuelve.
Declarar keys i18n en componentes
Agrega un objeto i18n a cualquier decorator de componente que lo soporte. Cada entrada apunta a la key de traducción que debe sobrescribir el texto estático de fallback en runtime. El valor estático sigue siendo importante: se convierte en el fallback que se muestra cuando el archivo de locale no define esa key.
Este modelo fallback-first es lo que hace práctica la traducción gradual. Puedes traducir un componente a la vez sin romper el resto de la UI.
import { Button } from '@spraxium/components';
@Button({
customId: 'confirm',
label: 'Confirm',
style: 'success',
i18n: {
label: 'buttons.confirm.label',
},
})
export class ConfirmButton {}import { SelectOption, StringSelect } from '@spraxium/components';
@StringSelect({
customId: 'topic_select',
placeholder: 'Choose a topic…',
i18n: { placeholder: 'select.topic.placeholder' },
})
@SelectOption({
label: 'Development',
value: 'dev',
description: 'Code and architecture topics',
i18n: {
label: 'select.topic.options.dev.label',
description: 'select.topic.options.dev.description',
},
})
@SelectOption({
label: 'Operations',
value: 'ops',
description: 'Deployment and infra topics',
i18n: {
label: 'select.topic.options.ops.label',
description: 'select.topic.options.ops.description',
},
})
export class TopicSelect {}buildLocalizedButton
Usa buildLocalizedButton cuando lo único que necesita tu handler es una action row localizada compuesta por clases de botón estáticas. Resuelve labels y keys relacionadas con emoji declaradas en cada botón y devuelve una única action row lista para enviar.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { buildLocalizedButton } from '@spraxium/i18n';
import type { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { ActionsCommand } from '../commands/actions.command';
import { ConfirmButton } from '../components/confirm-button.component';
import { CancelButton } from '../components/cancel-button.component';
@SlashCommandHandler(ActionsCommand)
export class ActionsCommandHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = await this.i18n.getUserLocale(interaction.user.id);
const row = buildLocalizedButton({ input: [ConfirmButton, CancelButton], locale });
await interaction.reply({ content: 'Choose an action:', components: [row] });
}
}Signature: buildLocalizedButton(options): ActionRowBuilder<ButtonBuilder>
| Opción | Tipo | Descripción |
|---|---|---|
| input | Class or Class[] | Una clase de botón o un array de clases de botón para colocar en la fila. |
| locale | string | Locale objetivo, como pt-BR. Si falta una key, se usa el label estático del decorator. |
Si ya tienes lógica de renderizado de botones específica de runtime, sigue usando las APIs de componentes dinámicos. El builder localizado funciona mejor cuando la estructura es estática y solo cambia el texto visible por locale.
buildLocalizedSelect
Usa buildLocalizedSelect para string selects cuyo placeholder, labels de opción y descripciones de opción provienen de archivos de traducción. La función es async porque los metadatos del select pueden requerir resolución asíncrona de locale.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { buildLocalizedSelect } from '@spraxium/i18n';
import type { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { TopicCommand } from '../commands/topic.command';
import { TopicSelect } from '../components/topic-select.component';
@SlashCommandHandler(TopicCommand)
export class TopicCommandHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = await this.i18n.getUserLocale(interaction.user.id);
const row = await buildLocalizedSelect({ selectClass: TopicSelect, locale });
await interaction.reply({ content: 'Pick a topic:', components: [row] });
}
}Signature: buildLocalizedSelect(options): Promise<ActionRowBuilder<AnySelectBuilder>>
| Opción | Tipo | Descripción |
|---|---|---|
| selectClass | Class | La clase de componente decorada con @StringSelect. |
| locale | string | El locale cuyo placeholder y textos de opción deben resolverse. |
Este builder encaja bien para conjuntos de opciones estables como listas de temas, elecciones de onboarding o menús de ajustes. Si la lista de opciones en sí cambia en runtime, combina localización con las APIs de select dinámico en lugar de forzar todas las posibilidades dentro de metadatos estáticos del decorator.
buildLocalizedModal
Usa buildLocalizedModal cuando la estructura del modal es fija pero el título, labels y placeholders deben cambiar por locale. El título del modal y el custom ID vienen de los metadatos de @ModalComponent, mientras que labels y placeholders de campos se resuelven desde decorators de campo.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { buildLocalizedModal } from '@spraxium/i18n';
import type { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { FeedbackCommand } from '../commands/feedback.command';
import { FeedbackModal } from '../components/feedback-modal.component';
@SlashCommandHandler(FeedbackCommand)
export class FeedbackCommandHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = await this.i18n.getUserLocale(interaction.user.id);
await interaction.showModal(buildLocalizedModal({ modalClass: FeedbackModal, locale }));
}
}Signature: buildLocalizedModal(options): ModalBuilder
| Opción | Tipo | Descripción |
|---|---|---|
| modalClass | Class | La clase de componente decorada con @ModalComponent. |
| locale | string | El locale usado para resolver título, labels y placeholders del modal. |
Esto mantiene los handlers especialmente limpios en flujos de feedback, formularios, pasos de onboarding y cualquier modal abierto desde botones o slash commands.
buildLocalizedEmbed
Usa buildLocalizedEmbed cuando la forma del embed es estable y solo cambia el texto visible por locale o por datos de plantilla. Resuelve título, descripción, nombres de fields y valores de fields en tiempo de llamada, y luego interpola cualquier variable suministrada por data.
import { Embed, EmbedField } from '@spraxium/components';
@Embed({
color: 0x5865f2,
i18n: { title: 'embeds.stats.title' },
})
@EmbedField({
name: 'Guilds',
value: '{{guilds}}',
inline: true,
i18n: { name: 'embeds.stats.fields.guilds' },
})
export class StatsEmbed {}import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { buildLocalizedEmbed } from '@spraxium/i18n';
import type { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { StatsCommand } from '../commands/stats.command';
import { StatsEmbed } from '../components/stats-embed.component';
@SlashCommandHandler(StatsCommand)
export class StatsCommandHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = await this.i18n.getUserLocale(interaction.user.id);
const embed = buildLocalizedEmbed({
embedClass: StatsEmbed,
locale,
data: { guilds: String(interaction.client.guilds.cache.size) },
});
await interaction.reply({ embeds: [embed] });
}
}Signature: buildLocalizedEmbed(options): EmbedBuilder
| Opción | Tipo | Descripción |
|---|---|---|
| embedClass | Class | La clase de componente decorada con @Embed. |
| locale | string | El locale usado para resolver título, descripción y texto de fields. |
| data | Record<string, string> | Variables opcionales para llenar placeholders como o . |
Es especialmente útil para tarjetas de estado, dashboards y embeds de resumen donde el mismo layout se reutiliza entre locales y solo varían las strings de contenido.
buildLocalizedV2
Usa buildLocalizedV2 cuando tu UI está construida con contenedores V2 y quieres el mismo flujo guiado por locale que usan los demás builders. Resuelve keys i18n en bloques de texto, secciones y componentes embebidos, y luego delega el render en V2Service.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import type { V2Service } from '@spraxium/components';
import { buildLocalizedV2 } from '@spraxium/i18n';
import type { I18nService } from '@spraxium/i18n';
import { MessageFlags, type ChatInputCommandInteraction } from 'discord.js';
import { ProfileCommand } from '../commands/profile.command';
import { ProfileContainer } from '../schemas/profile.container';
@SlashCommandHandler(ProfileCommand)
export class ProfileCommandHandler {
constructor(
private readonly i18n: I18nService,
private readonly v2: V2Service,
) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = await this.i18n.getUserLocale(interaction.user.id);
const data = { username: interaction.user.username };
const reply = buildLocalizedV2({
containerClass: ProfileContainer,
locale,
v2Service: this.v2,
data,
});
await interaction.reply({ ...reply, flags: reply.flags | MessageFlags.Ephemeral });
}
}Signature: buildLocalizedV2(options): V2ReplyPayload
| Opción | Tipo | Descripción |
|---|---|---|
| containerClass | Class | La clase decorada con @V2Container. |
| locale | string | El locale cuyas strings deben resolverse en todo el árbol del contenedor. |
| v2Service | V2Service | El renderer inyectado usado para construir el payload final V2. |
| data | Record<string, string> | Variables opcionales para interpolación de placeholders dentro de strings localizadas. |
Recurre a este builder cuando quieras que un esquema V2 sirva para múltiples locales sin duplicar clases de contenedor.
Referencia de builders
| Función | Devuelve | Asíncrono | Mejor para |
|---|---|---|---|
| buildLocalizedButton | ActionRowBuilder<ButtonBuilder> | No | Filas de botones estáticas cuyo texto cambia por locale. |
| buildLocalizedSelect | ActionRowBuilder<AnySelectBuilder> | Sí | String select menus localizados con metadatos de opciones estáticos. |
| buildLocalizedModal | ModalBuilder | No | Formularios cuyos labels y placeholders varían por locale. |
| buildLocalizedEmbed | EmbedBuilder | No | Layouts de embed reutilizables con texto traducido y valores interpolados. |
| buildLocalizedV2 | V2ReplyPayload | No | Contenedores V2 y payloads de respuesta localizados. |
Todos los builders hacen fallback al valor estático definido en el decorator cuando el locale solicitado no contiene la key. Los bots parcialmente traducidos siguen funcionando, y las keys faltantes degradan al texto predeterminado en lugar de romper el flujo de interacción.
Guía práctica
Usa los builders localizados cuando la estructura de la UI es estable y el texto es lo que cambia. Si tanto la estructura como el contenido varían en runtime, conserva el builder localizado para las partes traducidas y combínalo con las APIs de componentes dinámicos cuando haga falta.
Esa separación normalmente mantiene el código más fácil de mantener:
- Los decorators definen la forma estable del componente.
- Los archivos de locale definen el texto visible.
- Los handlers deciden solo cuándo construir y enviar el componente.