Por qué importa un sistema de i18n estructurado
La mayoría de los bots empiezan con strings en inglés hardcodeadas y dispersas en archivos de handlers, y eso funciona bien para los primeros cientos de usuarios. El problema aparece cuando la comunidad crece e incluye guilds en Brasil, España, Japón o Corea: el único camino para dar soporte a esos usuarios es rastrear manualmente cada string en todo el codebase y envolverla en lógica condicional. Ese enfoque no escala, se rompe con los refactors y mezcla preocupaciones de presentación directamente en la lógica de comandos.
El módulo i18n de Spraxium resuelve esto con un registro centralizado de strings traducidas cargado una sola vez en el arranque, un pipeline de resolución que selecciona automáticamente el locale correcto para cada interacción y helpers que conectan localizaciones de slash commands directamente desde el decorator del comando. Escribir un bot internacionalizado se parece casi igual a escribir uno monolingüe, porque la complejidad la absorbe el framework en lugar de quedar dispersa entre métodos de handlers.
La localización no trata solo de reemplazar texto. Trata de respetar que los usuarios tienen un idioma preferido, que Discord tiene su propio sistema de locale que afecta cómo se muestran los comandos en la UI, y que las reglas de pluralización varían de forma significativa entre idiomas. El módulo maneja las tres preocupaciones en un sistema cohesivo, así que rara vez tienes que pensar en la mecánica mientras escribes handlers.
Cómo funciona la resolución de locale
Cuando un handler llama a tUser, el servicio resuelve el locale de ese usuario mediante una cadena de prioridad de tres niveles: una preferencia de usuario guardada tiene prioridad sobre una preferencia de guild guardada, y una preferencia de guild guardada queda por encima del locale predeterminado configurado. Este orden es el modelo correcto para bots de Discord porque respeta primero las decisiones individuales, luego los ajustes a nivel comunidad, y cae al valor predeterminado del bot solo cuando no se ha definido ninguna preferencia.
Las preferencias de usuario y guild se almacenan en el backend activo de I18nStore y sobreviven a reinicios del bot siempre que el backend sea persistente. El MemoryStore integrado funciona para desarrollo local y bots que no necesitan preferencias persistentes, mientras que en producción conviene usar un store respaldado por base de datos como los mostrados en la página de la API del Store.
Las localizaciones de slash commands siguen un camino diferente: cuando el bot registra comandos con la REST API de Discord, los nombres y descripciones pueden tener variantes por locale que Discord renderiza de forma nativa en el selector de comandos. El módulo i18n conecta esas localizaciones automáticamente durante la secuencia de arranque, por lo que declarar un campo i18n en un decorator de slash es todo lo necesario para producir una entrada de comando completamente localizada en la UI de Discord.
Configuración del módulo
import { defineI18n } from '@spraxium/i18n';
export const i18nConfig = defineI18n({
defaultLocale: 'en-US',
references: [
{ name: 'commands', path: './locales/commands' },
{ name: 'messages', path: './locales/messages' },
{ name: 'errors', path: './locales/errors' },
],
});Agrega el objeto de configuración al array plugins de tu configuración de Spraxium. El módulo es autocontenido
y registra su propio I18nLifecycle, que se ejecuta automáticamente en el arranque antes de que se registren
los slash commands.
| Campo | Tipo | Finalidad |
|---|---|---|
| defaultLocale | string | Locale de fallback usado cuando no existe preferencia de usuario o guild |
| references | Array | Punteros nombrados a directorios de locales en disco |
| pluralRules | Record | Funciones de plural opcionales por locale, sobrescribiendo Intl.PluralRules |
API de I18nService
Inyecta I18nService en cualquier handler o provider. Expone métodos de traducción síncronos y asíncronos para cubrir cualquier patrón de uso que necesite tu bot.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { PingCommand } from '../commands/ping.command';
@SlashCommandHandler(PingCommand)
export class PingHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = interaction.locale;
const message = this.i18n.t('commands.ping.response', locale);
await interaction.reply(message);
}
}| Método | Asíncrono | Qué hace |
|---|---|---|
| t(key, locale, vars?) | No | Traduce una key para un locale explícito con sustitución opcional de variables |
| tp(key, locale, count, vars?) | No | Traduce con pluralización para un locale explícito |
| tUser(userId, key, vars?, guildId?) | Sí | Traduce después de resolver el locale por la cadena de preferencias de usuario y guild |
| tpUser(userId, key, count, vars?, guildId?) | Sí | Traducción pluralizada después de resolver el locale por la cadena de preferencias |
| setUserLocale(userId, locale) | Sí | Persiste una preferencia de locale para un usuario |
| setGuildLocale(guildId, locale) | Sí | Persiste una preferencia de locale para una guild |
| getUserLocale(userId) | Sí | Recupera el locale guardado del usuario o el predeterminado |
| getGuildLocale(guildId) | Sí | Recupera el locale guardado de la guild o el predeterminado |
| resolveLocale(userId, guildId?) | Sí | Ejecuta la cadena completa de resolución y devuelve el locale ganador |