Por que preferências de locale precisam de uma store persistente
I18nService.tUser() resolve o locale de uma pessoa seguindo uma cadeia de prioridade: preferência do usuário, depois preferência da guild e, por fim, o locale padrão configurado. Para essa cadeia continuar funcionando depois de um restart do bot, alguém precisa lembrar essas preferências. A store é exatamente esse componente. Ela é a interface responsável por persistir e recuperar a escolha de locale feita por usuários e guilds.
O motivo de a store existir como peça separada e intercambiável, em vez de ser uma estrutura em memória hardcoded, é simples: preferências de locale têm o mesmo requisito de persistência de qualquer outra configuração visível para a pessoa usuária. Quem escolhe português hoje espera continuar em português amanhã, mesmo depois de o bot reiniciar. Isso exige um backend que sobreviva a reinícios de processo. O backend ideal depende do que o bot já usa para persistência no restante do projeto, e é por isso que a store é uma interface, não uma implementação fixa.
Como o I18nStoreRegistry funciona
I18nStoreRegistry é uma classe estática que guarda a referência da store ativa. O lifecycle de i18n sempre chama I18nStoreRegistry.get() durante o boot para recuperar a store, exista ou não uma implementação customizada registrada. Se nenhuma store customizada tiver sido definida, o registry cria e devolve automaticamente uma instância de MemoryStore. Não há erro e nenhuma configuração extra é exigida para o uso básico.
O ponto mais importante desse desenho é que o registro da store é independente do objeto de configuração do plugin. Você chama I18nStoreRegistry.set(yourStore) em qualquer momento anterior à execução do lifecycle de i18n. Pode ser no bootstrap principal, em um hook customizado de lifecycle ou em qualquer outro ponto da sequência de startup. O registry mantém essa referência até o bot desligar ou até reset() ser chamado.
Registrando uma store customizada
import { I18nStoreRegistry } from '@spraxium/i18n';
import { IntentPreset, SpraxiumFactory } from '@spraxium/core';
import { DrizzleLocaleStore } from './stores/drizzle-locale.store';
import { db } from './database/connection';
async function main(): Promise<void> {
I18nStoreRegistry.set(new DrizzleLocaleStore(db));
const app = await SpraxiumFactory.create({
token: process.env.DISCORD_TOKEN!,
});
app.intents(IntentPreset.Standard);
await app.listen();
}
main();A store é registrada logo no topo de main, antes de SpraxiumFactory.create() ser chamado. Isso garante que ela já esteja disponível quando o lifecycle de i18n rodar. Colocar esse registro no bootstrap principal também torna a dependência explícita e fácil de localizar.
Implementando uma store customizada
Qualquer classe que implemente I18nStore é uma store válida. A interface exige cinco métodos. Os exemplos abaixo mostram implementações equivalentes usando bibliotecas de banco bastante comuns, para que você adapte o padrão ao ORM já usado no projeto.
import type { I18nStore } from '@spraxium/i18n';
import { eq } from 'drizzle-orm';
import { localePreferences, guildLocalePreferences } from '../db/schema';
export class DrizzleLocaleStore implements I18nStore {
constructor(private readonly db: DrizzleDatabase) {}
async init(): Promise<void> {
// Run migrations, create tables, or perform connection checks here.
// Called once at boot by I18nLifecycle, not on every translation request.
}
async getUser(userId: string): Promise<string | undefined> {
const row = await this.db
.select()
.from(localePreferences)
.where(eq(localePreferences.userId, userId))
.limit(1);
return row[0]?.locale ?? undefined;
}
async setUser(userId: string, locale: string): Promise<void> {
await this.db
.insert(localePreferences)
.values({ userId, locale })
.onConflictDoUpdate({ target: localePreferences.userId, set: { locale } });
}
async getGuild(guildId: string): Promise<string | undefined> {
const row = await this.db
.select()
.from(guildLocalePreferences)
.where(eq(guildLocalePreferences.guildId, guildId))
.limit(1);
return row[0]?.locale ?? undefined;
}
async setGuild(guildId: string, locale: string): Promise<void> {
await this.db
.insert(guildLocalePreferences)
.values({ guildId, locale })
.onConflictDoUpdate({ target: guildLocalePreferences.guildId, set: { locale } });
}
}| Método | Quando é chamado | Contrato |
|---|---|---|
| init() | Uma vez no boot, via I18nLifecycle | Setup e migrations. Pode lançar erro. Falha aqui aborta o boot. |
| getUser(userId) | Em toda chamada de tUser() ou resolveLocale() | Deve retornar a string do locale salvo ou undefined. Pode ser assíncrono. |
| setUser(userId, locale) | Quando a pessoa usuária executa um comando de troca de locale | Persiste a preferência. Deve sobrescrever o valor anterior. |
| getGuild(guildId) | Em toda chamada de tUser() ou resolveLocale() sem pref de usuário | Deve retornar a string do locale salvo ou undefined. Pode ser assíncrono. |
| setGuild(guildId, locale) | Quando uma pessoa admin executa um comando de troca de locale | Persiste a preferência da guild. Deve sobrescrever o valor anterior. |
Usando a MemoryStore embutida no desenvolvimento
MemoryStore é o fallback padrão. Ela guarda preferências em dois Maps criados do zero a cada início do processo e perdidos quando ele termina. Não exige setup, banco nem variável de ambiente. Para desenvolvimento local, ou para bots em que preferência de locale não seja uma funcionalidade relevante, MemoryStore é a escolha certa: o sistema funciona normalmente, só que as preferências não sobrevivem a reinícios.
Você nunca precisa registrar MemoryStore explicitamente. Ela entra automaticamente quando nenhuma store customizada for registrada. Se quiser deixar isso explícito por clareza de documentação ou em testes, ainda assim pode chamar I18nStoreRegistry.set(new MemoryStore()) em qualquer ponto do startup.
Um comando de troca de locale que grava na store
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { LocaleCommand } from '../commands/locale.command';
@SlashCommandHandler(LocaleCommand)
export class LocaleHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = interaction.options.getString('locale', true);
const userId = interaction.user.id;
await this.i18n.setUserLocale(userId, locale);
const confirmation = await this.i18n.tUser(
userId,
'commands.locale.changed',
{ locale },
);
await interaction.reply({ content: confirmation, flags: 'Ephemeral' });
}
}I18nService.setUserLocale() delega diretamente para store.setUser(). A próxima chamada de tUser() desse mesmo usuário já será resolvida no novo locale. Não existe cache para invalidar porque a cadeia de prioridade sempre consulta a store a cada chamada.
Resetando a store em testes
Como I18nStoreRegistry é estático, estado pode vazar de um teste para outro. Use reset() nos hooks de teardown para manter os casos isolados:
import { I18nStoreRegistry, MemoryStore } from '@spraxium/i18n';
beforeEach(() => {
I18nStoreRegistry.set(new MemoryStore());
});
afterEach(() => {
I18nStoreRegistry.reset();
});