Por que um sistema estruturado de i18n importa
A maioria dos bots começa com strings em inglês hardcoded, espalhadas pelos arquivos de handler, e isso funciona bem nos primeiros centenas de usuários. O problema aparece quando a comunidade cresce e passa a incluir guilds do Brasil, da Espanha, do Japão ou da Coreia: o único caminho para dar suporte a essas pessoas passa a ser rastrear manualmente cada string no codebase e envolver tudo em lógica condicional. Essa abordagem não escala, quebra em refactors e mistura preocupações de apresentação diretamente na lógica dos comandos.
O módulo de i18n do Spraxium resolve isso com um registro centralizado de strings traduzidas, carregado uma vez no boot, um pipeline de resolução que escolhe automaticamente o locale certo para cada interação e helpers que conectam localizações de slash commands diretamente pelo decorator do comando. Escrever um bot internacionalizado fica quase igual a escrever um bot monolíngue, porque a complexidade é absorvida pelo framework, em vez de ficar espalhada pelos handlers.
Localização não é só substituição de texto. Também envolve respeitar o fato de que usuários têm um idioma preferido, que o próprio Discord tem um sistema de locale que afeta como os comandos aparecem na UI e que regras de pluralização variam bastante entre idiomas. O módulo cuida dessas três preocupações em um sistema coeso, então você quase nunca precisa pensar na mecânica enquanto escreve handlers.
Como a resolução de locale funciona
Quando um handler chama tUser, o serviço resolve o locale daquele usuário por meio de uma cadeia de prioridade em três níveis: uma preferência salva do usuário vence uma preferência salva da guild, e uma preferência salva da guild vence o locale padrão configurado. Essa ordem é o modelo correto para bots do Discord porque respeita primeiro a escolha individual, depois a configuração em nível de comunidade e só cai no padrão global do bot quando nenhuma preferência foi definida.
As preferências de usuário e guild ficam armazenadas no backend ativo de I18nStore e sobrevivem a reinicializações do bot, desde que o backend seja persistente. O MemoryStore embutido funciona bem para desenvolvimento local e para bots que não precisam persistir preferências, enquanto bots de produção devem usar uma store baseada em banco, como as mostradas na página da API da Store.
As localizações de slash commands seguem um caminho diferente: quando o bot registra comandos pela REST API do Discord, nomes e descrições podem ter variantes por locale, que o Discord renderiza nativamente no seletor de comandos. O módulo de i18n conecta essas localizações automaticamente durante a sequência de boot, então basta declarar um campo i18n em um decorator de slash para produzir uma entrada de comando totalmente localizada na UI do Discord.
Configurando o 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' },
],
});Adicione o objeto de configuração ao array plugins na sua configuração do Spraxium. O módulo é autocontido e registra seu próprio I18nLifecycle, que roda automaticamente no boot antes que qualquer slash command seja registrado.
| Campo | Tipo | Finalidade |
|---|---|---|
| defaultLocale | string | Locale de fallback usado quando nenhuma preferência de usuário ou guild foi definida |
| references | Array | Ponteiros nomeados para diretórios de locale no disco |
| pluralRules | Record | Funções de plural opcionais por locale, sobrescrevendo Intl.PluralRules |
A API do I18nService
Injete I18nService em qualquer handler ou provider. Ele expõe métodos síncronos e assíncronos de tradução para cobrir todos os padrões de uso de que seu bot precisa.
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 | Async | O que faz |
|---|---|---|
| t(key, locale, vars?) | Não | Traduz uma chave para um locale explícito com substituição opcional de variáveis |
| tp(key, locale, count, vars?) | Não | Traduz com pluralização para um locale explícito |
| tUser(userId, key, vars?, guildId?) | Sim | Traduz depois de resolver o locale pela cadeia de preferência de usuário e guild |
| tpUser(userId, key, count, vars?, guildId?) | Sim | Traduz com pluralização depois de resolver o locale pela cadeia de preferências |
| setUserLocale(userId, locale) | Sim | Persiste uma preferência de locale para um usuário |
| setGuildLocale(guildId, locale) | Sim | Persiste uma preferência de locale para uma guild |
| getUserLocale(userId) | Sim | Recupera o locale salvo do usuário ou o locale padrão |
| getGuildLocale(guildId) | Sim | Recupera o locale salvo da guild ou o locale padrão |
| resolveLocale(userId, guildId?) | Sim | Executa a cadeia completa de resolução e retorna o locale vencedor |