A lightweight TypeScript template for building Discord bots with discord.js and Bun.
- Clone the repository and install dependencies:
git clone https://github.com/bur4ky/bot-template.git
cd bot-template
bun install
cp .env.example .env-
Fill in
.env. -
Register slash commands:
bun run register- Start the bot:
bun run startCommands are defined with defineCommand() for type safety.
When using localization, do not set command, subcommand or option descriptions in SlashCommandBuilder.
The command loader automatically applies the default descriptions and all localized metadata from the commandData localization files during registration.
Example:
defineCommand({
data: new SlashCommandBuilder()
.setName('ping')
.addStringOption((o) => o.setName('target').setAutocomplete(true)),
config: {
category: 'bot'
},
run: async ({ interaction, t }) => {
return sendSuccess(interaction, t('ping.pong'));
},
autocomplete: async ({ interaction, t }) => {
const focused = interaction.options.getFocused();
return interaction.respond(getMatches(focused));
}
});Translations live in src/locales/<code>/.
To add a new language:
- Add all namespace files under
src/locales/<code>/. - Add the locale mapping to
languagesinsrc/shared/config.ts. - Update the types
src/i18next.d.tsif you're adding a new namespace.
The language code used as the value in languages must match the folder name under src/locales/.
Each command's run() and autocomplete() receives a t function already scoped to the commands namespace so you don't need to import i18next or specify lng manually:
run: async ({ interaction, t }) => {
return sendSuccess(interaction, t('ping.pong'));
}commandData.json contains the localized names, descriptions and choice labels used when registering slash commands to Discord.
Every top-level key must match the original English name passed to .setName() and must not be translated. Only the name, description and choice labels should be localized.
Example (tr/commandData.json):
{
"deep": {
"name": "derin",
"description": "Derin bir komut örneği",
"options": {
"subcommand-group-name": {
"name": "alt-komut-grubu",
"description": "Alt komut grubu örneği",
"options": {
"subcommand-name": {
"name": "alt-komut",
"description": "Alt komut örneği",
"options": {
"option-name": {
"name": "seçenek",
"description": "Seçenek örneği",
"choices": {
"your-choice-value1": "Seçenek 1",
"your-choice-value2": "Seçenek 2"
}
}
}
}
}
}
}
}
}It's recommended to use these helpers instead of calling interaction.reply/editReply directly for consistency:
await sendSuccess(interaction, t('ping.pong'));
await sendError(interaction, { description: t('botAdminsOnly', { ns: 'errors' }) });