Qué es una referencia y por qué existe
Una referencia es el puente entre un directorio en disco y un namespace en el espacio de keys de traducción. Declaras una referencia dándole un nombre y una ruta. El nombre se convierte en el primer segmento de cada key que proviene de ese directorio. La ruta apunta a un directorio cuya estructura sigue una de dos convenciones: archivos JSON planos por locale, o subdirectorios por locale que contienen varios archivos JSON. Ambas estructuras son válidas y pueden coexistir entre distintas referencias dentro del mismo bot.
La razón por la que existen las referencias como concepto, en lugar de un único directorio plano de locales, es que los bots reales tienen preocupaciones de traducción diferentes que pertenecen a lugares diferentes. Los nombres y descripciones de slash commands son objetos de dominio en la misma categoría que el código. Los mensajes de error usados en todo el bot comparten un namespace. Las strings de UI para modales, select menus y embeds son otra capa. Mezclar todo esto en un único archivo por locale crea sobrecarga de mantenimiento y dificulta identificar qué falta y dónde. Las referencias le dan a cada preocupación su propio directorio y su propio prefijo, sin dejar de servir todas las búsquedas desde el mismo map plano en memoria.
Esta separación es una decisión de experiencia de desarrollo, no de rendimiento en runtime. En runtime, todas las referencias se fusionan en el mismo map plano por locale. El nombre de referencia se convierte en un prefijo de key que evita colisiones, pero no hay una búsqueda separada por referencia ni recorrido de rutas de directorio en tiempo de petición.
Declarar múltiples referencias
import { defineI18n } from '@spraxium/i18n';
export const i18nConfig = defineI18n({
defaultLocale: 'en-US',
references: [
{ name: 'slash', path: './locales/slash' },
{ name: 'ui', path: './locales/ui' },
{ name: 'errors', path: './locales/errors' },
{ name: 'embeds', path: './locales/embeds' },
],
});No existe límite para el número de referencias. Cada una se carga de forma independiente y se fusiona en el
mismo registro. Mientras cada valor de name sea único, no puede haber colisiones de keys entre
referencias.
Estructura de archivos plana
En este diseño, cada locale es un único archivo JSON en la raíz del directorio de referencia. El nombre del archivo es el identificador de locale y su contenido es el objeto de traducción para ese locale.
{
'notFound': 'Resource not found',
'forbidden': 'You do not have permission to do this',
'timeout': 'The operation timed out after {{seconds}} seconds'
}Después de cargar la referencia { name: 'errors', path: './locales/errors' }, las keys disponibles en
el registro para en-US son:
errors.notFounderrors.forbiddenerrors.timeout
El nivel de anidación dentro del archivo JSON no importa. Las estructuras profundamente anidadas se aplanan recursivamente con notación de punto uniendo cada nivel.
Estructura de directorios anidada
Cuando una referencia contiene muchos archivos, o cuando quieres organizar traducciones por funcionalidad dentro de
un locale, puedes usar subdirectorios. Cada subdirectorio se nombra con un locale y contiene
varios archivos JSON. El nombre del archivo (sin .json) se convierte en un segmento de namespace adicional.
{
'name': 'ping',
'description': "Check the bot's response time",
'options': {
'target': {
'name': 'target',
'description': 'The server or service to ping'
}
}
}Después de cargar la referencia { name: 'slash', path: './locales/slash' }, las keys disponibles para
en-US incluyen:
slash.ping.nameslash.ping.descriptionslash.ping.options.target.nameslash.ping.options.target.descriptionslash.help.nameslash.settings.name- (y todas las demás keys de todos los demás archivos)
El nombre de archivo ping se convierte en el segundo segmento después de slash, y las keys del JSON se vuelven
los segmentos siguientes. La ruta completa siempre es: referenceName.fileName.jsonKeyPath.
Mezclar ambas estructuras
Puedes mezclar libremente diseños planos y anidados entre diferentes referencias en la misma configuración.
ReferenceLoader detecta qué diseño usar a nivel de referencia comprobando si las
entradas dentro del directorio de referencia son archivos o subdirectorios.
import { defineI18n } from '@spraxium/i18n';
export const i18nConfig = defineI18n({
defaultLocale: 'en-US',
references: [
// Nested layout: many files per locale, organized by feature
{ name: 'slash', path: './locales/slash' },
// Flat layout: simple key-value pairs in a single file per locale
{ name: 'errors', path: './locales/errors' },
{ name: 'system', path: './locales/system' },
],
});Acceder a keys entre referencias
Todas las referencias se acceden desde la misma API del servicio sin especificar a qué referencia pertenece una key. El nombre de la referencia forma parte de la key.
import { Ctx, SlashCommandHandler } from '@spraxium/common';
import { I18nService } from '@spraxium/i18n';
import type { ChatInputCommandInteraction } from 'discord.js';
import { StatusCommand } from '../commands/status.command';
@SlashCommandHandler(StatusCommand)
export class StatusHandler {
constructor(private readonly i18n: I18nService) {}
async handle(@Ctx() interaction: ChatInputCommandInteraction): Promise<void> {
const locale = interaction.locale;
const userId = interaction.user.id;
const guildId = interaction.guildId ?? undefined;
// Keys from 'slash' reference
const title = this.i18n.t('slash.status.embed.title', locale);
// Keys from 'errors' reference, used when an upstream check fails
const permError = this.i18n.t('errors.forbidden', locale);
// Async resolution through user/guild preference chain
const footer = await this.i18n.tUser(
userId,
'slash.status.embed.footer',
{},
guildId,
);
await interaction.reply({ content: `${title}: ${footer}` });
}
}Usar keys de referencia en decorators de slash
Cuando agregas un campo i18n a un decorator de slash, las rutas de key apuntan al map plano cargado. Usan el nombre de referencia como primer segmento, exactamente igual que en una llamada a t().
import { SlashCommand } from '@spraxium/common';
@SlashCommand({
name: 'settings',
description: 'Manage bot settings',
i18n: {
name: 'slash.settings.name',
description: 'slash.settings.description',
},
})
export class SettingsCommand {}El lifecycle de i18n ya cargó el map plano cuando corre el registrador de slash. El bridge lee el registro para cada locale de Discord soportado para el que tu bot tenga traducción y pasa los resultados a los métodos builder de discord.js. Entonces el comando aparece en la UI de Discord con nombres localizados de forma nativa para cada locale que hayas proporcionado.
Qué pasa cuando falta un locale
Cuando se solicita una key para un locale para el que el bot no tiene archivo, el registro cae silenciosamente al map del locale predeterminado. Si la key tampoco existe en el locale predeterminado, se devuelve la string cruda de la key. El sistema nunca lanza excepciones por traducciones faltantes en tiempo de petición. Degrada de forma elegante en producción y emite un log de nivel trace para ayudar a detectar huecos durante desarrollo.
La implicación práctica es que puedes publicar traducciones parciales. Si tienes inglés y
portugués y quieres agregar español en una versión futura, puedes añadir los archivos es-ES de forma incremental.
Los usuarios con preferencia de locale en español recibirán respuestas en inglés (el predeterminado) hasta que los
archivos en español estén completos. Nada se rompe. El fallback simplemente es visible para esos usuarios.