Skip to content

Commit

Permalink
+Translation Support
Browse files Browse the repository at this point in the history
Ported over translation capabilities.
  • Loading branch information
sethwalker1 committed Nov 27, 2023
1 parent 34353ca commit 5daff4b
Show file tree
Hide file tree
Showing 16 changed files with 2,397 additions and 57 deletions.
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,23 @@ CLIENT_ID=
DISCORD_TOKEN=
# URL for the logging webhook
LOGGING_WEBHOOK_URL=

# Detect Language API
# Token for the Detect Language API
DETECT_LANGUAGE_TOKEN=

# DeepL API
# Token for the DeepL translation API
DEEPL_TOKEN=

# Microsoft Translation API
# Resource key for the Microsoft Translator Text API
TRANSLATOR_TEXT_RESOURCE_KEY=
# Region for the Microsoft Translator Text API
TRANSLATOR_TEXT_REGION=
# Endpoint for the Microsoft Translator Text API
TRANSLATOR_TEXT_ENDPOINT=

# Pirate API
# URL for the Pirate API
PIRATE_API_URL=https://pirate.monkeyness.com/api
67 changes: 38 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Discord Bot Framework
# Translation Discord Bot: Your Go-To Multilingual Assistant

A scalable Discord bot framework: easily expandable with modular components.
This is a barebones template. If you want to see something more fleshed out,
check out
[this translation bot fork](https://github.com/sethwalker1/Translation-Discord-Bot).
This is an (unofficial) fork of
[Discord Bot Framework](https://github.com/sethwalker1/Discord-Bot-Framework),
also developed by me.

**Tired of subpar translation bots on Discord? Look no further!**

Translation bots are a dime a dozen, but finding a good one? That's where
thechallenge lies. Most have high subscription costs associated with due to
implementing advanced Deep Learning models. One solution is to host it yourself,
using your own API token(s) for a free and accessible translation solution.

I made this in about two days because some of my friends couldn't find a single
free translation bot worth using. This bot aims to be just that. It's fast,
reliable, and easy to use.

## Getting Started

Expand Down Expand Up @@ -31,36 +41,35 @@ check out

## Features

### Commands
### Automatic Translation

The bot will automatically translate any message not matching the configured
language in the `.env` file.

### Manual Translation

Commands are defined in the `src/commands` directory. Each command is a class
that inherits from the `Command` class. The `Command` class provides a simple
interface for defining commands and subcommands.
React with a country's flag emoji to translate a message manually. Flag codes
and languages are in `src/modules/Emoji/`.
### Subcommands
### Customization
To create a subcommand, you need to follow a few steps.
Configure up to three translation APIs in the `.env` file, supporting over 130
languages and allowing for 2.5 million characters monthly.
1. Create a folder matching the base command's name in
`src/commands/subcommands` and create a new `.mjs` file matching the
subcommand's name.
2. Inside your new subcommand file, export an object with:
- `data`: An arrow method that is passed as an argument to the
`addSubcommand` method.
- `execute`: An async class method that is called when the subcommand is
executed.
#### Detect Language
> 💡 The `execute` method in the `Command` class supports loading subcommands by default. You're need to implement the logic yourself if your command overrides the `execute` method.
[This API](https://detectlanguage.com/) is optional but recommended. It
identifies message languages, saving translation characters when the source
language matches the default. Offers 1000 free daily **requests**, with no
character limit!
### Events
#### DeepL
Events are defined in the `src/events` directory. Each event is an object, not a
class. The `execute` method is called when the event is triggered.
[DeepL](https://www.deepl.com/) provides high-quality translations using Deep
Learning. Free for up to 500,000 characters monthly across 29 languages.
The `messageCreate` event can dynamically load message handler modules. To
create a message handler:
#### Microsoft Translator
1. Create a new `.mjs` file in the `src/events/messageHandlers` directory
2. Inside your new message handler file, export a method with your custom logic.
3. Your method needs to filter each message and return `null` when the message
doesn't match your criteria, as it will be executed for every message.
[Microsoft Translator](https://microsoft.com/en-us/translator/) offers 2 million
free characters monthly for personal use, supporting 120+ languages. Requires
more setup effort but maximizes your free character allowance.
11 changes: 7 additions & 4 deletions index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import path from 'path';
import * as Sentry from '@sentry/node';
import { ProfilingIntegration } from '@sentry/profiling-node';
import Client from './src/Client.mjs';
import Translate from './src/modules/Translate/Translate.mjs';

// Load .env file
const productionEnvPath = path.join(process.cwd(), '.env.production');
dotenv.config({
path: path.join(
process.cwd(),
fs.existsSync(productionEnvPath) ? productionEnvPath : '.env'
),
path: fs.existsSync(productionEnvPath)
? productionEnvPath
: path.join(process.cwd(), '.env'),
});

// Override .env file with .development.env file if in development mode
Expand Down Expand Up @@ -49,6 +49,9 @@ async function init() {
profilesSampleRate: 1.0,
});

// Initiate translation engines
await Translate.initEngines();

// Initiate Discord client
await Client.init();
}
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
"@sentry/node": "^7.80.1",
"@sentry/profiling-node": "^1.2.6",
"cross-env": "^7.0.3",
"deepl-node": "^1.11.0",
"detectlanguage": "^2.1.0",
"discord.js": "^14.9.0",
"dotenv": "^16.3.1"
"dotenv": "^16.3.1",
"dotenv-vault": "^1.25.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"nodemon": "^2.0.22"
Expand Down
11 changes: 6 additions & 5 deletions src/Client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export default class Client {
GatewayIntentBits.DirectMessages,
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions,
],
partials: [Partials.Message, Partials.User],
partials: [Partials.Message, Partials.Reaction, Partials.User],
});
Client.client.commands = new Collection();

Expand All @@ -47,16 +48,16 @@ export default class Client {

// Load and validate each command asynchronously for better performance
return await Promise.all(
commandFiles.map(async file => {
commandFiles.map(file => new Promise(async resolve => {
const filePath = path.join(directory, file);
const { default: command } = await import(`file://${filePath}`);

// All valid commands must have a data and execute property
if (!('data' in command && 'execute' in command))
return console.warn(`The command at ${filePath} is invalid!`);

return command;
})
resolve(command);
}))
);
}

Expand Down
20 changes: 20 additions & 0 deletions src/events/messageHandlers/translate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Translate from '../../modules/Translate/Translate.mjs';

export default async message => {
const { content } = message;

// Translate any non-English message to English
if (message.author.bot) return null;

// Translate the message
const { text, embed } = (await Translate.translate(content, 'en')) ?? {};

// If the message couldn't be translated, don't send a response
if (!text || !embed) return;

// Send the response
await message.reply({
embeds: [embed],
allowedMentions: { repliedUser: false },
});
};
32 changes: 32 additions & 0 deletions src/events/messageReactionAdd.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Events } from 'discord.js';
import Emoji from '../modules/Emoji/Emoji.mjs';
import Translate from '../modules/Translate/Translate.mjs';

export default {
name: Events.MessageReactionAdd,
async execute(reaction, user) {
// When a reaction is received, check if the structure is partial
if (reaction.partial)
// If the message this reaction belongs to was removed, the fetching might result in an API error which should be handled
await reaction.fetch().catch(() => {});

if (!reaction.message?.content) return;

// Convert the emoji to a language code
const targetLanguage = Emoji.emojiToLanguage(reaction.emoji.name);
if (!targetLanguage) return;

// Translate the message
const { embed } =
(await Translate.translate(reaction.message.content, targetLanguage)) ??
{};

if (!embed) return;

// Send the response
await reaction.message.reply({
embeds: [embed],
allowedMentions: { repliedUser: false },
});
},
};
Loading

0 comments on commit 5daff4b

Please sign in to comment.