From 2b4a120dae6ec346dce8793f6d67e6e769eb32c9 Mon Sep 17 00:00:00 2001 From: Ignacio Miranda Figueroa <38511917+IgnacioNMiranda@users.noreply.github.com> Date: Thu, 30 Dec 2021 12:39:42 -0600 Subject: [PATCH] feat: i18n (#42) * chore: config files for sapphire/i18n * feat: i18n for misc commands * fix: cancelgame change file name * feat: redefinition of locales getters with t function, games commands i18n * fix: use of args.t for i18n in misc commands * feat: i18n for roles related commands * feat: i18n for listeners and preconditions * refactor: preconditions names * feat: i18n for utils * fix: replace Users for username strings in i18n texts * feat: selectLanguage command * refactor: configuration keys * feat: selectLanguage command * feat: spanish translations for listeners, preconditions, categories, errors and roleAssignment messages * feat: spanish i18n for commands * fix: selectlanguages's ignore state * chore: version 2.4.0 --- .prettierrc | 3 +- package.json | 6 +- src/app.ts | 80 +- src/commands/games/cancelGame.ts | 46 - src/commands/games/cancelgame.ts | 43 + src/commands/games/tictactoe.ts | 883 +++++++++--------- src/commands/games/tictactoeleaderboard.ts | 33 +- src/commands/misc/congratulate.ts | 85 +- src/commands/misc/help.ts | 240 ++--- .../misc/{initChibiKnight.ts => init.ts} | 52 +- src/commands/misc/say.ts | 58 +- src/commands/misc/selectlanguage.ts | 70 ++ src/commands/misc/shameonyou.ts | 86 +- .../{activateRoles.ts => activateroles.ts} | 90 +- src/commands/roles/myRole.ts | 86 -- src/commands/roles/myrole.ts | 93 ++ src/commands/roles/roles.ts | 65 +- src/config/index.ts | 36 +- src/database/cache.ts | 9 +- src/database/database.ts | 9 +- src/database/models/guild.model.ts | 6 + src/database/services/guild.service.ts | 6 +- src/database/services/user.service.ts | 19 +- src/languages/en-US/categories.json | 5 + src/languages/en-US/commands/games.json | 42 + src/languages/en-US/commands/misc.json | 24 + src/languages/en-US/commands/roles.json | 37 + src/languages/en-US/errors.json | 3 + src/languages/en-US/listeners/commands.json | 6 + src/languages/en-US/listeners/guild.json | 3 + src/languages/en-US/preconditions/roles.json | 4 + src/languages/en-US/preconditions/server.json | 4 + src/languages/en-US/preconditions/user.json | 3 + src/languages/en-US/rolesAssignment.json | 4 + src/languages/es-ES/categories.json | 5 + src/languages/es-ES/commands/games.json | 42 + src/languages/es-ES/commands/misc.json | 24 + src/languages/es-ES/commands/roles.json | 36 + src/languages/es-ES/errors.json | 3 + src/languages/es-ES/listeners/commands.json | 6 + src/languages/es-ES/listeners/guild.json | 3 + src/languages/es-ES/preconditions/roles.json | 4 + src/languages/es-ES/preconditions/server.json | 4 + src/languages/es-ES/preconditions/user.json | 3 + src/languages/es-ES/rolesAssignment.json | 4 + src/listeners/commands/commandError.ts | 21 +- src/listeners/guild/guildCreate.ts | 15 +- src/listeners/guild/guildDelete.ts | 18 +- src/listeners/messages/messageCreate.ts | 39 +- src/listeners/system/ready.ts | 4 +- ...lesActiveOnly.ts => RolesActivatedOnly.ts} | 17 +- ...tActiveOnly.ts => RolesDeactivatedOnly.ts} | 17 +- ...nitializeOnly.ts => BotInitializedOnly.ts} | 8 +- ...ializeOnly.ts => BotNotInitializedOnly.ts} | 8 +- src/preconditions/user/AdminOnly.ts | 8 +- src/utils/command.ts | 31 + src/utils/functions/buttons.ts | 19 +- src/utils/functions/files/index.ts | 5 +- src/utils/functions/games/index.ts | 6 +- src/utils/functions/games/tictactoe.ts | 3 +- src/utils/i18n/index.ts | 26 + src/utils/i18n/keys/categories.ts | 5 + .../i18n/keys/commands/games/basePath.ts | 1 + .../i18n/keys/commands/games/cancelgame.ts | 5 + src/utils/i18n/keys/commands/games/index.ts | 7 + .../i18n/keys/commands/games/tictactoe.ts | 29 + .../commands/games/tictactoeleaderboard.ts | 8 + src/utils/i18n/keys/commands/index.ts | 3 + src/utils/i18n/keys/commands/misc/basePath.ts | 1 + .../i18n/keys/commands/misc/congratulate.ts | 4 + src/utils/i18n/keys/commands/misc/help.ts | 7 + src/utils/i18n/keys/commands/misc/index.ts | 6 + src/utils/i18n/keys/commands/misc/init.ts | 5 + src/utils/i18n/keys/commands/misc/say.ts | 3 + .../i18n/keys/commands/misc/selectlanguage.ts | 6 + .../i18n/keys/commands/misc/shameonyou.ts | 4 + .../i18n/keys/commands/roles/activateroles.ts | 13 + .../i18n/keys/commands/roles/basePath.ts | 1 + src/utils/i18n/keys/commands/roles/index.ts | 3 + src/utils/i18n/keys/commands/roles/myrole.ts | 12 + src/utils/i18n/keys/commands/roles/roles.ts | 13 + src/utils/i18n/keys/errors.ts | 3 + src/utils/i18n/keys/index.ts | 6 + src/utils/i18n/keys/listeners/commands.ts | 6 + src/utils/i18n/keys/listeners/guild.ts | 3 + src/utils/i18n/keys/listeners/index.ts | 2 + src/utils/i18n/keys/preconditions/index.ts | 3 + src/utils/i18n/keys/preconditions/roles.ts | 4 + src/utils/i18n/keys/preconditions/server.ts | 4 + src/utils/i18n/keys/preconditions/user.ts | 3 + src/utils/i18n/keys/rolesAssignment.ts | 4 + src/utils/i18n/locales.ts | 22 + src/utils/index.ts | 4 +- src/utils/links/commands.ts | 4 +- src/utils/links/utils.ts | 6 +- src/utils/logger.ts | 16 +- src/utils/objects/commands.ts | 14 +- src/utils/objects/games/tictactoe.ts | 20 +- src/utils/objects/roles.ts | 41 +- src/utils/role.util.ts | 64 +- src/utils/types/channel.ts | 15 +- src/utils/types/commands.ts | 7 +- src/utils/types/games/tictactoe.ts | 3 + src/utils/types/index.ts | 1 + src/utils/types/preconditions.ts | 20 + src/utils/types/roles.ts | 2 + src/utils/types/types.d.ts | 14 +- tsconfig.json | 3 +- yarn.lock | 42 + 109 files changed, 1801 insertions(+), 1289 deletions(-) delete mode 100644 src/commands/games/cancelGame.ts create mode 100644 src/commands/games/cancelgame.ts rename src/commands/misc/{initChibiKnight.ts => init.ts} (56%) create mode 100644 src/commands/misc/selectlanguage.ts rename src/commands/roles/{activateRoles.ts => activateroles.ts} (56%) delete mode 100644 src/commands/roles/myRole.ts create mode 100644 src/commands/roles/myrole.ts create mode 100644 src/languages/en-US/categories.json create mode 100644 src/languages/en-US/commands/games.json create mode 100644 src/languages/en-US/commands/misc.json create mode 100644 src/languages/en-US/commands/roles.json create mode 100644 src/languages/en-US/errors.json create mode 100644 src/languages/en-US/listeners/commands.json create mode 100644 src/languages/en-US/listeners/guild.json create mode 100644 src/languages/en-US/preconditions/roles.json create mode 100644 src/languages/en-US/preconditions/server.json create mode 100644 src/languages/en-US/preconditions/user.json create mode 100644 src/languages/en-US/rolesAssignment.json create mode 100644 src/languages/es-ES/categories.json create mode 100644 src/languages/es-ES/commands/games.json create mode 100644 src/languages/es-ES/commands/misc.json create mode 100644 src/languages/es-ES/commands/roles.json create mode 100644 src/languages/es-ES/errors.json create mode 100644 src/languages/es-ES/listeners/commands.json create mode 100644 src/languages/es-ES/listeners/guild.json create mode 100644 src/languages/es-ES/preconditions/roles.json create mode 100644 src/languages/es-ES/preconditions/server.json create mode 100644 src/languages/es-ES/preconditions/user.json create mode 100644 src/languages/es-ES/rolesAssignment.json rename src/preconditions/roles/{RolesActiveOnly.ts => RolesActivatedOnly.ts} (54%) rename src/preconditions/roles/{RolesNotActiveOnly.ts => RolesDeactivatedOnly.ts} (53%) rename src/preconditions/server/{BotInitializeOnly.ts => BotInitializedOnly.ts} (51%) rename src/preconditions/server/{BotNotInitializeOnly.ts => BotNotInitializedOnly.ts} (51%) create mode 100644 src/utils/command.ts create mode 100644 src/utils/i18n/index.ts create mode 100644 src/utils/i18n/keys/categories.ts create mode 100644 src/utils/i18n/keys/commands/games/basePath.ts create mode 100644 src/utils/i18n/keys/commands/games/cancelgame.ts create mode 100644 src/utils/i18n/keys/commands/games/index.ts create mode 100644 src/utils/i18n/keys/commands/games/tictactoe.ts create mode 100644 src/utils/i18n/keys/commands/games/tictactoeleaderboard.ts create mode 100644 src/utils/i18n/keys/commands/index.ts create mode 100644 src/utils/i18n/keys/commands/misc/basePath.ts create mode 100644 src/utils/i18n/keys/commands/misc/congratulate.ts create mode 100644 src/utils/i18n/keys/commands/misc/help.ts create mode 100644 src/utils/i18n/keys/commands/misc/index.ts create mode 100644 src/utils/i18n/keys/commands/misc/init.ts create mode 100644 src/utils/i18n/keys/commands/misc/say.ts create mode 100644 src/utils/i18n/keys/commands/misc/selectlanguage.ts create mode 100644 src/utils/i18n/keys/commands/misc/shameonyou.ts create mode 100644 src/utils/i18n/keys/commands/roles/activateroles.ts create mode 100644 src/utils/i18n/keys/commands/roles/basePath.ts create mode 100644 src/utils/i18n/keys/commands/roles/index.ts create mode 100644 src/utils/i18n/keys/commands/roles/myrole.ts create mode 100644 src/utils/i18n/keys/commands/roles/roles.ts create mode 100644 src/utils/i18n/keys/errors.ts create mode 100644 src/utils/i18n/keys/index.ts create mode 100644 src/utils/i18n/keys/listeners/commands.ts create mode 100644 src/utils/i18n/keys/listeners/guild.ts create mode 100644 src/utils/i18n/keys/listeners/index.ts create mode 100644 src/utils/i18n/keys/preconditions/index.ts create mode 100644 src/utils/i18n/keys/preconditions/roles.ts create mode 100644 src/utils/i18n/keys/preconditions/server.ts create mode 100644 src/utils/i18n/keys/preconditions/user.ts create mode 100644 src/utils/i18n/keys/rolesAssignment.ts create mode 100644 src/utils/i18n/locales.ts create mode 100644 src/utils/types/preconditions.ts diff --git a/.prettierrc b/.prettierrc index c3481a7..db0a25a 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { "singleQuote": true, - "semi": false + "semi": false, + "printWidth": 120 } \ No newline at end of file diff --git a/package.json b/package.json index 4db4bd3..44b6621 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chibi-knight", - "version": "2.3.0", + "version": "2.4.0", "description": "A multipurpose discord bot", "main": "dist/app.js", "scripts": { @@ -8,7 +8,8 @@ "dev": "tsc-watch --onSuccess \"sh -c 'tsc-alias && node dist/app.js'\"", "build": "rm -rf dist && tsc && tsc-alias", "test": "echo \"Error: no test specified\" && exit 1", - "lint": "eslint \"{src, test}/**/*.{js,ts}\" --fix", + "lint": "eslint \"{src,test}/**/*.{js,ts}\" --fix", + "prettier": "prettier --write \"{src,test}/**/*.{js,ts}\"", "prepare": "husky install" }, "repository": { @@ -27,6 +28,7 @@ "private": "true", "dependencies": { "@sapphire/framework": "^2.2.2", + "@sapphire/plugin-i18next": "^2.1.4", "@sapphire/plugin-logger": "^2.1.1", "discord.js": "^13.4.0", "dotenv": "^10.0.0", diff --git a/src/app.ts b/src/app.ts index 83839bb..5cc3eed 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,45 +1,35 @@ -import { configuration } from '@/config' -import { container, SapphireClient } from '@sapphire/framework' -import { Cache, MongoDatabase } from '@/database' -import { logger } from '@/utils' -import '@sapphire/plugin-logger/register' - -const main = async () => { - const client = new SapphireClient({ - defaultPrefix: configuration.prefix, - intents: ['GUILDS', 'GUILD_MEMBERS', 'GUILD_MESSAGES'], - loadDefaultErrorListeners: false, - }) - - logger.info(`Initializing application...`, { - context: client.constructor.name, - }) - - logger.info(`Trying to connect to mongo database...`, { - context: client.constructor.name, - }) - try { - container.db = await MongoDatabase.connect() - container.cache = await Cache.init() - } catch (error) { - logger.error(`Database connection error. Could not connect to databases`, { - context: client.constructor.name, - }) - } - - try { - logger.info('Logging in...', { - context: client.constructor.name, - }) - await client.login(configuration.token) - } catch (error) { - const { code, method, path } = error - console.error(`Error ${code} trying to ${method} to ${path} path`) - } -} - -main().catch(() => - logger.error('Bot initialization failed', { - context: container.client.constructor.name, - }) -) +import { configuration } from '@/config' +import { container, SapphireClient } from '@sapphire/framework' +import { Cache, MongoDatabase } from '@/database' +import { i18nConfig, logger } from '@/utils' +import '@sapphire/plugin-logger/register' +import '@sapphire/plugin-i18next/register' + +const main = async () => { + const client = new SapphireClient({ + ...configuration.client, + intents: ['GUILDS', 'GUILD_MEMBERS', 'GUILD_MESSAGES'], + i18n: i18nConfig, + }) + + logger.info(`Initializing application...`, { + context: client.constructor.name, + }) + + logger.info(`Trying to connect to mongo database...`, { + context: client.constructor.name, + }) + container.db = await MongoDatabase.connect() + container.cache = await Cache.init() + + logger.info('Logging in...', { + context: client.constructor.name, + }) + await client.login(configuration.client.token) +} + +main().catch((reason) => { + logger.error(`Bot initialization failed. Error: ${reason}`, { + context: container.client.constructor.name, + }) +}) diff --git a/src/commands/games/cancelGame.ts b/src/commands/games/cancelGame.ts deleted file mode 100644 index c3cf738..0000000 --- a/src/commands/games/cancelGame.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Command, container } from '@sapphire/framework' -import type { Message } from 'discord.js' -import { logger } from '@/utils' - -/** - * Replies the receives message on command. - */ -export class CancelGameCommand extends Command { - constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - aliases: ['cg'], - fullCategory: ['games'], - description: 'Cancels the active game.', - preconditions: ['BotInitializeOnly'], - runIn: ['GUILD_ANY'], - }) - } - - /** - * It executes when someone types the "say" command. - */ - async messageRun(message: Message): Promise> { - const { id } = message.guild - - try { - const guild = await container.db.guildService.getById(id) - - if (guild && !guild.gameInstanceActive) { - return message.channel.send("There's no active game.") - } - - guild.gameInstanceActive = false - await guild.save() - return message.channel.send('Game cancelled.') - } catch (error) { - logger.error( - `(${this.constructor.name}): MongoDB Connection error. Could not change game instance state for '${message.guild.name}' server` - ) - } - - return message.channel.send( - 'It occured an unexpected error :sweat: try again later.' - ) - } -} diff --git a/src/commands/games/cancelgame.ts b/src/commands/games/cancelgame.ts new file mode 100644 index 0000000..e66ffdd --- /dev/null +++ b/src/commands/games/cancelgame.ts @@ -0,0 +1,43 @@ +import { Command, CommandOptionsRunTypeEnum, container } from '@sapphire/framework' +import type { Message } from 'discord.js' +import { logger, CustomPrecondition, languageKeys, CustomCommand, CustomArgs } from '@/utils' + +/** + * Replies the receives message on command. + */ +export class CancelGameCommand extends CustomCommand { + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['cg'], + description: languageKeys.commands.games.cancelgame.description, + preconditions: [CustomPrecondition.BotInitializedOnly], + runIn: [CommandOptionsRunTypeEnum.GuildAny], + }) + } + + /** + * It executes when someone types the "say" command. + */ + async messageRun(message: Message, { t }: CustomArgs): Promise> { + const { id } = message.guild + + try { + const guild = await container.db.guildService.getById(id) + + if (guild && !guild.gameInstanceActive) { + return message.channel.send(t(languageKeys.commands.games.cancelgame.noActiveGame)) + } + + guild.gameInstanceActive = false + await guild.save() + return message.channel.send(t(languageKeys.commands.games.cancelgame.gameCancelled)) + } catch (error) { + logger.error( + `(${this.constructor.name}): MongoDB Connection error. Could not change game instance state for '${message.guild.name}' server` + ) + } + + return message.channel.send(t(languageKeys.errors.unexpectedError)) + } +} diff --git a/src/commands/games/tictactoe.ts b/src/commands/games/tictactoe.ts index 3a03f53..c135557 100644 --- a/src/commands/games/tictactoe.ts +++ b/src/commands/games/tictactoe.ts @@ -1,460 +1,423 @@ -import { - Message, - MessageActionRow, - MessageEmbed, - User, - CollectorFilter, - MessageComponentInteraction, - ButtonInteraction, -} from 'discord.js' -import { Command, Args, container } from '@sapphire/framework' - -import { configuration } from '@/config' -import { DocumentType } from '@typegoose/typegoose' -import { User as DbUser, GuildData } from '@/database' -import { - commandsLinks, - logger, - defineRoles, - UserActions, - TicTacToeMoveResolverParams, - GameFinalState, - TicTacToeButtonId, - getButton, - getTttMoveButton, - getCancelGameButton, - tttGameResults, -} from '@/utils' - -/** - * Starts a tic-tac-toe game. - */ -export class TicTacToeCommand extends Command { - private readonly defaultSymbol = ':purple_square:' - private readonly squaresNumber = 9 - private board: string[] - private moveResolver: Record< - UserActions, - (_: TicTacToeMoveResolverParams) => Promise | void> - > = { - [UserActions.IGNORE]: this.ignore.bind(this), - [UserActions.REJECT]: this.reject.bind(this), - [UserActions.ACCEPT]: this.accept.bind(this), - } - - constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - aliases: ['ttt'], - fullCategory: ['games'], - description: 'Initiates a tictactoe game.', - preconditions: ['BotInitializeOnly'], - runIn: ['GUILD_ANY'], - }) - } - - /** - * It executes when someone types the "tictactoe" command. - */ - async messageRun(message: Message, args: Args): Promise { - // Obtains the challenging player instance. - const { author: player1 } = message - const player2 = await args.pick('user') - - const gameInstanceActive = ( - await container.db.guildService.getById(message.guild.id) - ).gameInstanceActive - - if (gameInstanceActive) { - return message.channel.send( - `There's already a game in course ${player1}!` - ) - } - /* if (player1.id === player2.id) { - return message.channel.send('You cannot challenge yourself ¬¬ ...') - } */ - if (player2.bot) { - return message.channel.send( - "You cannot challenge me :anger: I'm a superior being... I would destroy you n.n :purple_heart:" - ) - } - - const filter: CollectorFilter<[MessageComponentInteraction<'cached'>]> = ( - btnInteraction - ) => btnInteraction.user.id === player2.id - - const buttons = new MessageActionRow().addComponents( - getButton(TicTacToeButtonId.ACCEPT, 'YES', 'SUCCESS'), - getButton(TicTacToeButtonId.REJECT, 'NO', 'DANGER') - ) - - const actionMessage = await message.channel.send({ - content: `${player2} has been challenged by ${player1} to play #. Do you accept the challenge?`, - components: [buttons], - }) - - // Waits 15 seconds while player2 clicks button. - const collector = message.channel.createMessageComponentCollector({ - filter, - max: 1, - time: 1000 * 15, - }) - - let player2Action = UserActions.IGNORE - collector.on('end', async (collection) => { - if (collection.first()?.customId === TicTacToeButtonId.REJECT) { - player2Action = UserActions.REJECT - } else if (collection.first()?.customId === TicTacToeButtonId.ACCEPT) { - player2Action = UserActions.ACCEPT - } - - this.moveResolver[player2Action]({ message, player2, player1 }).then( - (message?: Message) => { - actionMessage.delete().catch() - setTimeout(() => { - message?.delete().catch() - }, 1000 * 3) - } - ) - }) - } - - async ignore({ message, player2 }: TicTacToeMoveResolverParams) { - return message.channel.send( - `Time's up! ${player2.username} doesn't want to play ):` - ) - } - - reject({ message, player2 }: TicTacToeMoveResolverParams) { - return message.channel.send( - `Game cancelled ): Come back when you are brave enough, ${player2}.` - ) - } - - async accept({ message, player2, player1 }: TicTacToeMoveResolverParams) { - try { - const { id } = message.guild - const guild = await container.db.guildService.getById(id) - guild.gameInstanceActive = true - await guild.save() - - this.runTtt(message, player1, player2) - } catch (error) { - logger.error( - `MongoDB Connection error. Could not register change game instance active state for ${message.guild.name}`, - { - context: this.constructor.name, - } - ) - return message.channel.send( - 'Error ): could not start game. Try again later :purple_heart:' - ) - } - } - - /** - * Manages the tictactoe game and its properties. - */ - async runTtt(message: Message, player1: User, player2: User) { - const availableMoves = [...Array(this.squaresNumber).keys()] - this.board = Array(this.squaresNumber).fill(this.defaultSymbol) - const { id: guildId } = message.guild - - let { activePlayer, otherPlayer } = this.getInitialOrder(player1, player2) - - const embedMessage = this.embedDefaultboard(player1, player2) - .addField( - 'Instructions', - 'Type the number where you want to make your move.', - false - ) - .addField('Current turn', `It's your turn ${activePlayer.username}`) - - const firstMovesRow = new MessageActionRow().addComponents( - availableMoves - .slice(0, (availableMoves.length + 1) / 2) - .map(getTttMoveButton) - ) - - const secondMovesRow = new MessageActionRow() - .addComponents( - availableMoves - .slice((availableMoves.length + 1) / 2, availableMoves.length) - .map(getTttMoveButton) - ) - .addComponents(getCancelGameButton(TicTacToeButtonId.CANCEL)) - - const sentEmbedMessage = await message.channel.send({ - embeds: [embedMessage], - components: [firstMovesRow, secondMovesRow], - }) - - const moveFilter: CollectorFilter< - [MessageComponentInteraction<'cached'>] - > = (btnInteraction) => { - const playerId = [player1.id, player2.id].find( - (id) => id === btnInteraction.user.id - ) - if (!playerId) return false - if (btnInteraction.customId === TicTacToeButtonId.CANCEL) { - collector.stop('Game cancelled.') - return true - } - - return btnInteraction.user.id === activePlayer.id - } - - const collector = message.channel.createMessageComponentCollector({ - filter: moveFilter, - max: 9, - }) - - collector.on('collect', async (i: ButtonInteraction) => { - const playedNumber = parseInt(i.component.label) - - // If player1 made a move, the mark is :x:. If it was player2, the mark is :o: - activePlayer.id === player1.id - ? (this.board[playedNumber] = ':x:') - : (this.board[playedNumber] = ':o:') - - // Change the play turn. - ;[activePlayer, otherPlayer] = [otherPlayer, activePlayer] - - const updatedButtons = sentEmbedMessage.components.map((row) => { - return { - ...row, - components: row.components.filter( - (btn) => btn.customId !== i.customId - ), - } - }) - - // Creates the new embed message with the new mark. - const newEmbedMessage = this.embedDefaultboard(player1, player2) - - // Obtains the current state of the game. - const gameState = this.obtainGameState(playedNumber) - - if (gameState !== GameFinalState.UNDEFINED) { - let winner: User - let loser: User - - if (gameState === GameFinalState.PLAYER1_VICTORY) - [winner, loser] = [player1, player2] - else if (gameState === GameFinalState.PLAYER2_VICTORY) - [winner, loser] = [player2, player1] - - if (winner) { - try { - logger.info( - `Registering ${winner.username}'s tictactoe victory in '${i.guild.name}' guild...`, - { - context: this.constructor.name, - } - ) - - const score = 10 - let finalScore = score - const user: DocumentType = - await container.db.userService.getById(winner.id) - if (user) { - const guildDataIdx = user.guildsData.findIndex( - (guildData) => guildData.guildId === guildId - ) - user.guildsData[guildDataIdx].participationScore += score - user.guildsData[guildDataIdx].tictactoeWins += 1 - finalScore = user.guildsData[guildDataIdx].participationScore - await user.save() - } else { - const guildData: GuildData = { - guildId, - participationScore: score, - } - const newUser: DbUser = { - discordId: winner.id, - name: winner.username, - guildsData: [guildData], - } - await container.db.userService.create(newUser) - } - - const authorGuildMember = await message.guild.members.fetch( - winner.id - ) - defineRoles(finalScore, authorGuildMember, message) - logger.info('Victory registered successfully', { - context: this.constructor.name, - }) - } catch (error) { - logger.error( - `MongoDB Connection error. Could not register ${winner.username}'s tictactoe victory`, - { - context: this.constructor.name, - } - ) - } - } - - const { result, stopReason } = tttGameResults[gameState]({ - player1, - player2, - }) - - newEmbedMessage.addField('Result :trophy:', result, false) - if (winner && loser) { - newEmbedMessage.addField( - 'Consolation prize :second_place:', - `Don't worry ${loser}, this is for you:` - ) - newEmbedMessage.setImage(commandsLinks.games.tictactoe.gifs[0]) - } else if (!winner && !loser) { - newEmbedMessage.addField( - 'Consolation prize :woozy_face:', - `You two are so bad, this is for you <3` - ) - newEmbedMessage.setImage(commandsLinks.games.tictactoe.gifs[0]) - } - - // Edits the embed tictactoe message. - await i.update({ - embeds: [newEmbedMessage], - components: [], - }) - - // Stop message collection. - collector.stop(stopReason) - } else { - newEmbedMessage - .addField( - 'Instructions', - 'Type the number where you want to make your move.', - false - ) - .addField( - 'Current turn', - `It's your turn ${activePlayer.username}`, - false - ) - - // Edits the embed message. - await i.update({ - embeds: [newEmbedMessage], - components: updatedButtons, - }) - } - }) - - collector.on('end', async (_: any, reason: any) => { - setTimeout(() => { - sentEmbedMessage.delete().catch() - }, 1000 * 10) - // Unlocks the game instance. - try { - const guild = await container.db.guildService.getById(guildId) - guild.gameInstanceActive = false - await guild.save() - } catch (error) { - logger.error( - `MongoDB Connection error. Could not change game instance active for ${message.guild.name} server`, - { - context: this.constructor.name, - } - ) - } - - logger.info(reason, { - context: this.constructor.name, - }) - }) - } - - /** - * Creates a generic tictactoe board. - */ - embedDefaultboard(player1: User, player2: User): MessageEmbed { - return new MessageEmbed() - .setTitle(`:x::o: Gato:3 :x::o:`) - .setColor(configuration.embedMessageColor) - .setDescription( - 'Tres en raya | Michi | Triqui | Gato | Cuadritos | Tictactoe | Whatever' - ) - .addField( - 'Challengers', - `${player1.username} V/S ${player2.username}`, - false - ) - .addField( - 'Board', - ` - ${this.board[0]}${this.board[1]}${this.board[2]} - ${this.board[3]}${this.board[4]}${this.board[5]} - ${this.board[6]}${this.board[7]}${this.board[8]} - `, - true - ) - .addField( - 'Positions reference', - ` - :zero::one::two: - :three::four::five: - :six::seven::eight: - `, - true - ) - } - - getInitialOrder(player1: User, player2: User) { - const random = Math.random() < 0.5 - return { - activePlayer: random ? player1 : player2, - otherPlayer: random ? player2 : player1, - } - } - - /** - * It resolves if the game is a tie or if someone has won. - */ - obtainGameState(playedNumber: number): GameFinalState { - // WHen game is cancelled - if (isNaN(playedNumber)) return GameFinalState.TIE - - const playedMark = this.board[playedNumber] - - // Depending of the played number, we check some row and column. - const initRowPos = playedNumber - (playedNumber % 3) - const initColPos = playedNumber % 3 - - const rowPositions = [ - this.board[initRowPos], - this.board[initRowPos + 1], - this.board[initRowPos + 2], - ] - - const colPositions = [ - this.board[initColPos], - this.board[initColPos + 3], - this.board[initColPos + 6], - ] - - const leftDiagonalPositions = [this.board[0], this.board[4], this.board[8]] - const rightDiagonalPositions = [this.board[2], this.board[4], this.board[6]] - - if ( - rowPositions.every((pos) => pos === playedMark) || - colPositions.every((pos) => pos === playedMark) || - leftDiagonalPositions.every((pos) => pos === playedMark) || - rightDiagonalPositions.every((pos) => pos === playedMark) - ) { - return playedMark === ':x:' - ? GameFinalState.PLAYER1_VICTORY - : GameFinalState.PLAYER2_VICTORY - } - - // Every slot is marked and no one has won. - if (this.board.every((mark) => mark !== this.defaultSymbol)) { - return GameFinalState.TIE - } - - return GameFinalState.UNDEFINED - } -} +import { + Message, + MessageActionRow, + MessageEmbed, + User, + CollectorFilter, + MessageComponentInteraction, + ButtonInteraction, +} from 'discord.js' +import { Command, container, CommandOptionsRunTypeEnum } from '@sapphire/framework' + +import { configuration } from '@/config' +import { DocumentType } from '@typegoose/typegoose' +import { User as DbUser, GuildData } from '@/database' +import { + commandsLinks, + logger, + defineRoles, + UserActions, + TicTacToeMoveResolverParams, + GameFinalState, + TicTacToeButtonId, + getButton, + getTttMoveButton, + getCancelGameButton, + tttGameResults, + CustomPrecondition, + languageKeys, + CustomArgs, + CustomCommand, +} from '@/utils' +import { TFunction } from '@sapphire/plugin-i18next' + +/** + * Starts a tic-tac-toe game. + */ +export class TicTacToeCommand extends CustomCommand { + private readonly defaultSymbol = ':purple_square:' + private readonly squaresNumber = 9 + private board: string[] + private moveResolver: Record Promise | void>> = { + [UserActions.IGNORE]: this.ignore.bind(this), + [UserActions.REJECT]: this.reject.bind(this), + [UserActions.ACCEPT]: this.accept.bind(this), + } + + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['ttt'], + description: languageKeys.commands.games.tictactoe.description, + preconditions: [CustomPrecondition.BotInitializedOnly], + runIn: [CommandOptionsRunTypeEnum.GuildAny], + }) + } + + /** + * It executes when someone types the "tictactoe" command. + */ + async messageRun(message: Message, args: CustomArgs): Promise { + // Obtains the challenging player instance. + const { author: player1 } = message + const player2 = await args.pick('user') + + const isGameInstanceActive = (await container.db.guildService.getById(message.guild.id)).gameInstanceActive + + const { + activeGameInstance, + challengeYourself, + challengeBot, + acceptChallengeButtonLabel, + rejectChallengeButtonLabel, + startGameQuestion, + } = languageKeys.commands.games.tictactoe + + if (isGameInstanceActive) { + return message.channel.send(args.t(activeGameInstance, { username: player1.username })) + } + if (player1.id === player2.id) { + return message.channel.send(args.t(challengeYourself)) + } + if (player2.bot) { + return message.channel.send(args.t(challengeBot)) + } + + const filter: CollectorFilter<[MessageComponentInteraction<'cached'>]> = (btnInteraction) => + btnInteraction.user.id === player2.id + + const buttons = new MessageActionRow().addComponents( + getButton(TicTacToeButtonId.ACCEPT, args.t(acceptChallengeButtonLabel), 'SUCCESS'), + getButton(TicTacToeButtonId.REJECT, args.t(rejectChallengeButtonLabel), 'DANGER') + ) + + const actionMessage = await message.channel.send({ + content: args.t(startGameQuestion, { player1Username: player1.username, player2Username: player2.username }), + components: [buttons], + }) + + // Waits 15 seconds while player2 clicks button. + const collector = message.channel.createMessageComponentCollector({ + filter, + max: 1, + time: 1000 * 15, + }) + + let player2Action = UserActions.IGNORE + collector.on('end', async (collection) => { + if (collection.first()?.customId === TicTacToeButtonId.REJECT) { + player2Action = UserActions.REJECT + } else if (collection.first()?.customId === TicTacToeButtonId.ACCEPT) { + player2Action = UserActions.ACCEPT + } + + this.moveResolver[player2Action]({ message, player2, player1, t: args.t }).then((message?: Message) => { + actionMessage.delete().catch() + setTimeout(() => { + message?.delete().catch() + }, 1000 * 3) + }) + }) + } + + async ignore({ message, player2, t }: TicTacToeMoveResolverParams) { + return message.channel.send( + t(languageKeys.commands.games.tictactoe.ignoreChallengeMessage, { player2Username: player2.username }) + ) + } + + async reject({ message, player2, t }: TicTacToeMoveResolverParams) { + return message.channel.send( + t(languageKeys.commands.games.tictactoe.rejectChallengeMessage, { username: player2.username }) + ) + } + + async accept({ message, player2, player1, t }: TicTacToeMoveResolverParams) { + try { + const { id } = message.guild + const guild = await container.db.guildService.getById(id) + guild.gameInstanceActive = true + await guild.save() + + this.runTtt(message, player1, player2, t) + } catch (error) { + console.log({ + error, + }) + + logger.error( + `MongoDB Connection error. Could not register change game instance active state for ${message.guild.name}`, + { + context: this.constructor.name, + } + ) + return message.channel.send(t(languageKeys.commands.games.tictactoe.errorMessage)) + } + } + + /** + * Manages the tictactoe game and its properties. + */ + async runTtt(message: Message, player1: User, player2: User, t: TFunction) { + const availableMoves = [...Array(this.squaresNumber).keys()] + this.board = Array(this.squaresNumber).fill(this.defaultSymbol) + const { id: guildId } = message.guild + + let { activePlayer, otherPlayer } = this.getInitialOrder(player1, player2) + + const { + instructionsTitle, + instructionsText, + currentTurnTitle, + currentTurnText, + resultText, + consolationPrizeTitleOneLoser, + consolationPrizeTextOneLoser, + consolationPrizeTitleTwoLosers, + consolationPrizeTextTwoLosers, + } = languageKeys.commands.games.tictactoe + + const embedMessage = this.embedDefaultboard(player1, player2, t) + .addField(t(instructionsTitle), t(instructionsText), false) + .addField(t(currentTurnTitle), t(currentTurnText, { activePlayerUsername: activePlayer.username })) + + const firstMovesRow = new MessageActionRow().addComponents( + availableMoves.slice(0, (availableMoves.length + 1) / 2).map(getTttMoveButton) + ) + + const secondMovesRow = new MessageActionRow() + .addComponents(availableMoves.slice((availableMoves.length + 1) / 2, availableMoves.length).map(getTttMoveButton)) + .addComponents(getCancelGameButton(TicTacToeButtonId.CANCEL, t)) + + const sentEmbedMessage = await message.channel.send({ + embeds: [embedMessage], + components: [firstMovesRow, secondMovesRow], + }) + + const moveFilter: CollectorFilter<[MessageComponentInteraction<'cached'>]> = (btnInteraction) => { + const playerId = [player1.id, player2.id].find((id) => id === btnInteraction.user.id) + if (!playerId) return false + if (btnInteraction.customId === TicTacToeButtonId.CANCEL) { + collector.stop('Game cancelled.') + return true + } + + return btnInteraction.user.id === activePlayer.id + } + + const collector = message.channel.createMessageComponentCollector({ + filter: moveFilter, + max: 9, + }) + + collector.on('collect', async (i: ButtonInteraction) => { + const playedNumber = parseInt(i.component.label) + + // If player1 made a move, the mark is :x:. If it was player2, the mark is :o: + activePlayer.id === player1.id ? (this.board[playedNumber] = ':x:') : (this.board[playedNumber] = ':o:') + + // Change the play turn. + ;[activePlayer, otherPlayer] = [otherPlayer, activePlayer] + + const updatedButtons = sentEmbedMessage.components.map((row) => { + return { + ...row, + components: row.components.filter((btn) => btn.customId !== i.customId), + } + }) + + // Creates the new embed message with the new mark. + const newEmbedMessage = this.embedDefaultboard(player1, player2, t) + + // Obtains the current state of the game. + const gameState = this.obtainGameState(playedNumber) + + if (gameState !== GameFinalState.UNDEFINED) { + let winner: User + let loser: User + + if (gameState === GameFinalState.PLAYER1_VICTORY) [winner, loser] = [player1, player2] + else if (gameState === GameFinalState.PLAYER2_VICTORY) [winner, loser] = [player2, player1] + + if (winner) { + try { + logger.info(`Registering ${winner.username}'s tictactoe victory in '${i.guild.name}' guild...`, { + context: this.constructor.name, + }) + + const score = 10 + let finalScore = score + const user: DocumentType = await container.db.userService.getById(winner.id) + if (user) { + const guildDataIdx = user.guildsData.findIndex((guildData) => guildData.guildId === guildId) + user.guildsData[guildDataIdx].participationScore += score + user.guildsData[guildDataIdx].tictactoeWins += 1 + finalScore = user.guildsData[guildDataIdx].participationScore + await user.save() + } else { + const guildData: GuildData = { + guildId, + participationScore: score, + } + const newUser: DbUser = { + discordId: winner.id, + name: winner.username, + guildsData: [guildData], + } + await container.db.userService.create(newUser) + } + + const authorGuildMember = await message.guild.members.fetch(winner.id) + defineRoles(finalScore, authorGuildMember, message) + logger.info('Victory registered successfully', { + context: this.constructor.name, + }) + } catch (error) { + logger.error(`MongoDB Connection error. Could not register ${winner.username}'s tictactoe victory`, { + context: this.constructor.name, + }) + } + } + + const { result, stopReason } = tttGameResults[gameState]({ + player1, + player2, + t, + }) + newEmbedMessage.addField(t(resultText), result, false) + if (winner && loser) { + newEmbedMessage.addField( + t(consolationPrizeTitleOneLoser), + t(consolationPrizeTextOneLoser, { loserUsername: loser.username }) + ) + newEmbedMessage.setImage(commandsLinks.games.tictactoe.gifs[0]) + } else if (!winner && !loser) { + newEmbedMessage.addField(t(consolationPrizeTitleTwoLosers), t(consolationPrizeTextTwoLosers)) + newEmbedMessage.setImage(commandsLinks.games.tictactoe.gifs[0]) + } + + // Edits the embed tictactoe message. + await i.update({ + embeds: [newEmbedMessage], + components: [], + }) + + // Stop message collection. + collector.stop(stopReason) + } else { + newEmbedMessage + .addField(t(instructionsTitle), t(instructionsText), false) + .addField(t(currentTurnTitle), t(currentTurnText, { activePlayerUsername: activePlayer.username }), false) + + // Edits the embed message. + await i.update({ + embeds: [newEmbedMessage], + components: updatedButtons, + }) + } + }) + + collector.on('end', async (_: any, reason: any) => { + setTimeout(() => { + sentEmbedMessage.delete().catch() + }, 1000 * 10) + // Unlocks the game instance. + try { + const guild = await container.db.guildService.getById(guildId) + guild.gameInstanceActive = false + await guild.save() + } catch (error) { + logger.error( + `MongoDB Connection error. Could not change game instance active for ${message.guild.name} server`, + { + context: this.constructor.name, + } + ) + } + + logger.info(reason, { + context: this.constructor.name, + }) + }) + } + + /** + * Creates a generic tictactoe board. + */ + embedDefaultboard(player1: User, player2: User, t: TFunction): MessageEmbed { + const { gameTitle, gameDescription, challengersTitle, challengersText, boardTitle, positionsReferenceTitle } = + languageKeys.commands.games.tictactoe + return new MessageEmbed() + .setTitle(t(gameTitle)) + .setColor(configuration.client.embedMessageColor) + .setDescription(t(gameDescription)) + .addField( + t(challengersTitle), + t(challengersText, { player1Username: player1.username, player2Username: player2.username }), + false + ) + .addField( + t(boardTitle), + ` + ${this.board[0]}${this.board[1]}${this.board[2]} + ${this.board[3]}${this.board[4]}${this.board[5]} + ${this.board[6]}${this.board[7]}${this.board[8]} + `, + true + ) + .addField( + t(positionsReferenceTitle), + ` + :zero::one::two: + :three::four::five: + :six::seven::eight: + `, + true + ) + } + + getInitialOrder(player1: User, player2: User) { + const random = Math.random() < 0.5 + return { + activePlayer: random ? player1 : player2, + otherPlayer: random ? player2 : player1, + } + } + + /** + * It resolves if the game is a tie or if someone has won. + */ + obtainGameState(playedNumber: number): GameFinalState { + // WHen game is cancelled + if (isNaN(playedNumber)) return GameFinalState.TIE + + const playedMark = this.board[playedNumber] + + // Depending of the played number, we check some row and column. + const initRowPos = playedNumber - (playedNumber % 3) + const initColPos = playedNumber % 3 + + const rowPositions = [this.board[initRowPos], this.board[initRowPos + 1], this.board[initRowPos + 2]] + + const colPositions = [this.board[initColPos], this.board[initColPos + 3], this.board[initColPos + 6]] + + const leftDiagonalPositions = [this.board[0], this.board[4], this.board[8]] + const rightDiagonalPositions = [this.board[2], this.board[4], this.board[6]] + + if ( + rowPositions.every((pos) => pos === playedMark) || + colPositions.every((pos) => pos === playedMark) || + leftDiagonalPositions.every((pos) => pos === playedMark) || + rightDiagonalPositions.every((pos) => pos === playedMark) + ) { + return playedMark === ':x:' ? GameFinalState.PLAYER1_VICTORY : GameFinalState.PLAYER2_VICTORY + } + + // Every slot is marked and no one has won. + if (this.board.every((mark) => mark !== this.defaultSymbol)) { + return GameFinalState.TIE + } + + return GameFinalState.UNDEFINED + } +} diff --git a/src/commands/games/tictactoeleaderboard.ts b/src/commands/games/tictactoeleaderboard.ts index 7cf7bf1..3be7658 100644 --- a/src/commands/games/tictactoeleaderboard.ts +++ b/src/commands/games/tictactoeleaderboard.ts @@ -1,29 +1,31 @@ import { Message, MessageEmbed } from 'discord.js' -import { Command, container } from '@sapphire/framework' +import { Command, CommandOptionsRunTypeEnum, container } from '@sapphire/framework' import { configuration } from '@/config' -import { logger } from '@/utils' +import { logger, CustomPrecondition, languageKeys, CustomCommand, CustomArgs } from '@/utils' import { User } from '@/database' /** * Displays the tictactoe game leaderboard. */ -export class TicTacToeLeaderBoardCommand extends Command { +export class TicTacToeLeaderBoardCommand extends CustomCommand { constructor(context: Command.Context, options: Command.Options) { super(context, { ...options, name: 'tttleaderboard', aliases: ['tttlb'], - fullCategory: ['games'], - description: 'Displays the tictactoe leaderboard.', - preconditions: ['BotInitializeOnly'], - runIn: ['GUILD_ANY'], + description: languageKeys.commands.games.tictactoeleaderboard.description, + preconditions: [CustomPrecondition.BotInitializedOnly], + runIn: [CommandOptionsRunTypeEnum.GuildAny], }) } /** * It executes when someone types the "tictactoeleaderboard" command. */ - async messageRun(message: Message): Promise { + async messageRun(message: Message, { t }: CustomArgs): Promise { + const { messageTitle, messageDescription, positioningLabel, victoriesLabel, errorMessage } = + languageKeys.commands.games.tictactoeleaderboard + try { const { id: guildId } = message.guild const topUsers = await container.db.userService.getByNestedFilter( @@ -33,9 +35,9 @@ export class TicTacToeLeaderBoardCommand extends Command { ) const leaderboard = new MessageEmbed() - .setTitle(`❌⭕ TicTacToe Leaderboard ❌⭕`) - .setColor(configuration.embedMessageColor) - .setDescription('Scoreboard of TicTacToe based on victories') + .setTitle(t(messageTitle)) + .setColor(configuration.client.embedMessageColor) + .setDescription(t(messageDescription)) const usernamesList: string[] = [] const scoreList = [] @@ -58,8 +60,9 @@ export class TicTacToeLeaderBoardCommand extends Command { scoreList.push(guildsData.tictactoeWins) position += 1 }) - leaderboard.addField('Positioning', usernamesList.join('\n'), true) - leaderboard.addField('Victories', scoreList.join('\n'), true) + + leaderboard.addField(t(positioningLabel), usernamesList.join('\n'), true) + leaderboard.addField(t(victoriesLabel), scoreList.join('\n'), true) return message.channel.send({ embeds: [leaderboard] }) } catch (error) { @@ -69,9 +72,7 @@ export class TicTacToeLeaderBoardCommand extends Command { context: this.constructor.name, } ) - return message.channel.send( - `Sorry ): I couldn't retrieve tictactoe leaderboard. I failed you :sweat:` - ) + return message.channel.send(t(errorMessage)) } } } diff --git a/src/commands/misc/congratulate.ts b/src/commands/misc/congratulate.ts index 61d41cb..5954519 100644 --- a/src/commands/misc/congratulate.ts +++ b/src/commands/misc/congratulate.ts @@ -1,41 +1,44 @@ -import { Args, Command } from '@sapphire/framework' -import { Message, MessageEmbed } from 'discord.js' -import { configuration } from '@/config' -import { commandsLinks } from '@/utils' - -/** - * Sends an embed message with congratulations to certain User and a celebration image. - */ -export class CongratulateCommand extends Command { - constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - aliases: ['c'], - fullCategory: ['misc'], - description: 'Congratulates some @User.', - runIn: ['GUILD_ANY'], - }) - } - - /** - * It executes when someone types the "congratulate" command. - */ - async messageRun(message: Message, args: Args): Promise { - const congratulatedPerson = await args.pick('user') - // Obtains the congratulation gif's urls. - const { gifs } = commandsLinks.misc.congratulate - const randIndex = Math.floor(Math.random() * gifs.length) - - const embedMessage = new MessageEmbed() - .setDescription(`Congratulations ${congratulatedPerson.username} !!`) - .setColor(configuration.embedMessageColor) - .setImage(gifs[randIndex]) - - try { - await message.channel.send({ embeds: [embedMessage] }) - return message.delete() - } catch (error) { - // If bot cannot delete messages. - } - } -} +import { Command, CommandOptionsRunTypeEnum } from '@sapphire/framework' +import { Message, MessageEmbed } from 'discord.js' +import { configuration } from '@/config' +import { commandsLinks, languageKeys, CustomCommand, CustomArgs } from '@/utils' + +/** + * Sends an embed message with congratulations to certain User and a celebration image. + */ +export class CongratulateCommand extends CustomCommand { + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['c'], + description: languageKeys.commands.misc.congratulate.description, + runIn: [CommandOptionsRunTypeEnum.GuildAny], + }) + } + + /** + * It executes when someone types the "congratulate" command. + */ + async messageRun(message: Message, args: CustomArgs): Promise { + const congratulatedPerson = await args.pick('user') + // Obtains the congratulation gif's urls. + const { gifs } = commandsLinks.misc.congratulate + const randIndex = Math.floor(Math.random() * gifs.length) + + const embedMessage = new MessageEmbed() + .setDescription( + args.t(languageKeys.commands.misc.congratulate.embedMessageDescription, { + username: congratulatedPerson.username, + }) + ) + .setColor(configuration.client.embedMessageColor) + .setImage(gifs[randIndex]) + + try { + await message.channel.send({ embeds: [embedMessage] }) + return message.delete() + } catch (error) { + // If bot cannot delete messages. + } + } +} diff --git a/src/commands/misc/help.ts b/src/commands/misc/help.ts index 587d824..c939475 100644 --- a/src/commands/misc/help.ts +++ b/src/commands/misc/help.ts @@ -1,120 +1,120 @@ -import { Args, Command } from '@sapphire/framework' -import { Message, MessageAttachment, MessageEmbed } from 'discord.js' -import { - BotChannel, - botLogoURL, - commandsCategoriesDescriptions, - getBotLogo, -} from '@/utils' -import { configuration } from '@/config' - -/** - * Sends an embed message with information of every existing command. - */ -export class HelpCommand extends Command { - constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - name: 'help', - aliases: ['h'], - fullCategory: ['misc'], - description: 'Gives information about every existing command.', - }) - } - - /** - * It executes when someone types the "help" command. - */ - async messageRun(message: Message, args: Args): Promise> { - const embedMessage = new MessageEmbed() - .setAuthor({ - name: configuration.appName, - iconURL: botLogoURL, - }) - .setThumbnail(botLogoURL) - .setColor(configuration.embedMessageColor) - - const commandName = await args.pick('string').catch(() => null) - - if (commandName) { - this.buildCommandHelp(commandName, message.channel) - } - - return this.buildHelpForEveryCommand(embedMessage, message.channel, [ - getBotLogo(), - ]) - } - - buildCommandHelp(commandName: string, channel: BotChannel) { - const command = this.store.get(commandName) as Command - // TODO: when implement localization - - if (command) { - /* let cmdArgs = '' - if (command.argsCollector) { - const { args } = command.argsCollector - if (args[0].type.id === 'user') cmdArgs = ' @User' - else if (args[0].type.id === 'string') cmdArgs = ` {${args[0].key}}` - } - - embedMessage.addField( - `${command.name.toLocaleUpperCase()}: `, - ` - **Description**: ${command.description} - **Aliases**: ${command.aliases} - **Parameters**: ${cmdArgs === '' ? 'None' : cmdArgs} - **Syntax**: ${configuration.prefix}${command.aliases[0]}${cmdArgs} - ` - ) - - embedMessage.setFooter( - `Type ${configuration.prefix}help to see a list with every available command.` - ) - return message.channel.send({ embeds: [embedMessage], files: [botLogo] }) */ - } - - return channel.send( - `Unknown command!! There are no commands with that name ):` - ) - } - - buildHelpForEveryCommand( - message: MessageEmbed, - channel: BotChannel, - files: MessageAttachment[] - ) { - message.setDescription( - `:crossed_swords: These are the available commands for Chibi Knight n.n` - ) - - const commands = this.store.container.stores.get('commands') - const categories = new Set() - - Array(...commands.values()).forEach((command) => { - if (command.enabled && !categories.has(command.category)) { - categories.add(command.category) - } - }) - - const fields = [...categories.values()].map((category) => ({ - category: commandsCategoriesDescriptions[category], - commands: commands - .filter((command) => command.category === category && command.enabled) - .map( - (cmd) => - `**${configuration.prefix}${cmd.name}** → ${ - cmd.description ?? 'No description was provided' - }` - ), - })) - - fields.forEach((field) => { - message.addField(field.category, field.commands.join('\n')) - }) - - message.setFooter( - `Type ${configuration.prefix}help {command} to see information about an specific command.` - ) - return channel.send({ embeds: [message], files }) - } -} +import { Command } from '@sapphire/framework' +import { Message, MessageAttachment, MessageEmbed } from 'discord.js' +import { + BotChannel, + botLogoURL, + commandsCategoriesDescriptions, + CustomCommand, + CustomArgs, + getBotLogo, + languageKeys, +} from '@/utils' +import { configuration } from '@/config' +import { TFunction } from '@sapphire/plugin-i18next' + +/** + * Sends an embed message with information of every existing command. + */ +export class HelpCommand extends CustomCommand { + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['h'], + description: languageKeys.commands.misc.help.description, + }) + } + + /** + * It executes when someone types the "help" command. + */ + async messageRun(message: Message, args: CustomArgs): Promise> { + const embedMessage = new MessageEmbed() + .setAuthor({ + name: configuration.appName, + iconURL: botLogoURL, + }) + .setThumbnail(botLogoURL) + .setColor(configuration.client.embedMessageColor) + + const commandName = await args.pick('string').catch(() => null) + + if (commandName) { + return this.buildCommandHelp(commandName, message.channel, args.t) + } + + return this.buildHelpForEveryCommand(embedMessage, message.channel, [getBotLogo()], args.t) + } + + async buildCommandHelp(commandName: string, channel: BotChannel, t: TFunction) { + const command = this.store.get(commandName) as Command + // TODO: when implement localization + + if (command) { + /* let cmdArgs = '' + if (command.argsCollector) { + const { args } = command.argsCollector + if (args[0].type.id === 'user') cmdArgs = ' @User' + else if (args[0].type.id === 'string') cmdArgs = ` {${args[0].key}}` + } + + embedMessage.addField( + `${command.name.toLocaleUpperCase()}: `, + ` + **Description**: ${command.description} + **Aliases**: ${command.aliases} + **Parameters**: ${cmdArgs === '' ? 'None' : cmdArgs} + **Syntax**: ${configuration.prefix}${command.aliases[0]}${cmdArgs} + ` + ) + + embedMessage.setFooter( + `Type ${configuration.prefix}help to see a list with every available command.` + ) + return message.channel.send({ embeds: [embedMessage], files: [botLogo] }) */ + } + + return channel.send(t(languageKeys.commands.misc.help.unknownCommandError)) + } + + async buildHelpForEveryCommand(message: MessageEmbed, channel: BotChannel, files: MessageAttachment[], t: TFunction) { + const { + appName, + client: { defaultPrefix: prefix }, + } = configuration + const { everyCommandEmbedMessageDescription, commandWithoutDescription, everyCommandEmbedMessageFooter } = + languageKeys.commands.misc.help + message.setDescription(t(everyCommandEmbedMessageDescription, { appName })) + + const commands = this.store.container.stores.get('commands') + const categories = new Set() + + Array(...commands.values()).forEach((command) => { + if (command.enabled && !categories.has(command.category)) { + categories.add(command.category) + } + }) + + const fieldsBuilder = [...categories.values()].map(async (category) => { + const commandsParsers = commands + .filter((command) => command.category === category && command.enabled) + .map(async (cmd) => { + const commandDescription = t(cmd.description, { appName }) ?? t(commandWithoutDescription) + return `**${configuration.client.defaultPrefix}${cmd.name}** → ${commandDescription}` + }) + const parsedCommands = await Promise.all(commandsParsers) + return { + category: commandsCategoriesDescriptions[category](t), + commands: parsedCommands, + } + }) + + const fields = await Promise.all(fieldsBuilder) + + fields.forEach((field) => { + message.addField(field.category, field.commands.join('\n')) + }) + + message.setFooter(t(everyCommandEmbedMessageFooter, { prefix })) + return channel.send({ embeds: [message], files }) + } +} diff --git a/src/commands/misc/initChibiKnight.ts b/src/commands/misc/init.ts similarity index 56% rename from src/commands/misc/initChibiKnight.ts rename to src/commands/misc/init.ts index d829ae7..6c2f358 100644 --- a/src/commands/misc/initChibiKnight.ts +++ b/src/commands/misc/init.ts @@ -1,21 +1,19 @@ import { Command, container } from '@sapphire/framework' import { Message } from 'discord.js' import { configuration } from '@/config' -import { logger } from '@/utils' +import { logger, CustomPrecondition, languageKeys, CustomCommand, CustomArgs } from '@/utils' import { Guild, User, GuildData } from '@/database' /** * Initialize bot funcionalities setting cache and adding server to BD. */ -export class InitChibiKnightCommand extends Command { +export class InitChibiKnightCommand extends CustomCommand { constructor(context: Command.Context, options: Command.Options) { super(context, { ...options, - name: 'init', aliases: ['i'], - fullCategory: ['misc'], - description: 'Initialize Chibi Knight funcionalities.', - preconditions: ['AdminOnly', 'BotNotInitializeOnly'], + description: languageKeys.commands.misc.init.description, + preconditions: [CustomPrecondition.AdminOnly, CustomPrecondition.BotNotInitializedOnly], requiredUserPermissions: ['ADMINISTRATOR'], }) } @@ -23,15 +21,19 @@ export class InitChibiKnightCommand extends Command { /** * It executes when someone types the "init" command. */ - async messageRun(message: Message): Promise { - try { - const { id: guildId, members } = message.guild + async messageRun(message: Message, { t }: CustomArgs): Promise { + const { id: guildId, members } = message.guild - logger.info(`Trying to register new server '${message.guild.name}'...`, { - context: this.constructor.name, - }) + logger.info(`Trying to register new server '${message.guild.name}'...`, { + context: this.constructor.name, + }) - const newGuild: Guild = { guildId } + const newGuild: Guild = { guildId } + const { + appName, + client: { defaultPrefix: prefix }, + } = configuration + try { await container.db.guildService.create(newGuild) logger.info(`'${message.guild.name}' guild registered succesfully`, { @@ -44,11 +46,7 @@ export class InitChibiKnightCommand extends Command { const guildData: GuildData = { guildId } const bdUser = await container.db.userService.getById(user.id) if (bdUser) { - if ( - !bdUser.guildsData.find( - (guildData) => guildData.guildId === guildId - ) - ) { + if (!bdUser.guildsData.find((guildData) => guildData.guildId === guildId)) { bdUser.guildsData.push(guildData) await bdUser.save() } @@ -62,20 +60,14 @@ export class InitChibiKnightCommand extends Command { } } }) - logger.info( - `'${message.guild.name}' users has been registered succesfully`, - { - context: this.constructor.name, - } - ) - return message.channel.send( - `${configuration.appName} has been initialize successfully :purple_heart: check out the commands with **${configuration.prefix}help** :smile:` - ) + logger.info(`'${message.guild.name}' users has been registered succesfully`, { + context: this.constructor.name, + }) + + return message.channel.send(t(languageKeys.commands.misc.init.initSuccessfullyEvent, { appName, prefix })) } catch (error) { logger.error(error, { context: this.constructor.name }) - return message.channel.send( - `It occured an unexpected error while trying to initialize ${configuration.appName} :sweat: try again later.` - ) + return message.channel.send(t(languageKeys.commands.misc.init.initSuccessfullyEvent, { appName })) } } } diff --git a/src/commands/misc/say.ts b/src/commands/misc/say.ts index e69d807..b0367a5 100644 --- a/src/commands/misc/say.ts +++ b/src/commands/misc/say.ts @@ -1,29 +1,29 @@ -import type { Message } from 'discord.js' -import { Command, Args } from '@sapphire/framework' - -/** - * Replies the receives message on command. - */ -export class SayCommand extends Command { - public constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - aliases: ['s'], - fullCategory: ['misc'], - description: 'Replies with the received message.', - }) - } - - /** - * It executes when someone types the "say" command. - */ - async messageRun(message: Message, args: Args): Promise> { - try { - const text = await args.pick('string') - await message.channel.send(text) - return message.delete() - } catch (error) { - // If bot cannot delete messages. - } - } -} +import type { Message } from 'discord.js' +import { Command } from '@sapphire/framework' +import { languageKeys, CustomCommand, CustomArgs } from '@/utils' + +/** + * Replies the receives message on command. + */ +export class SayCommand extends CustomCommand { + public constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['s'], + description: languageKeys.commands.misc.say.description, + }) + } + + /** + * It executes when someone types the "say" command. + */ + async messageRun(message: Message, args: CustomArgs): Promise> { + try { + const text = await args.pick('string') + await message.channel.send(text) + return message.delete() + } catch (error) { + // If bot cannot delete messages. + } + } +} diff --git a/src/commands/misc/selectlanguage.ts b/src/commands/misc/selectlanguage.ts new file mode 100644 index 0000000..7e8e82d --- /dev/null +++ b/src/commands/misc/selectlanguage.ts @@ -0,0 +1,70 @@ +import { Command, CommandOptionsRunTypeEnum } from '@sapphire/framework' +import { CustomCommand, CustomArgs, languageKeys, languagesTypes, getButton, CustomPrecondition } from '@/utils' +import { ButtonInteraction, Message, MessageActionRow } from 'discord.js' +import { resolveKey } from '@sapphire/plugin-i18next' + +/** + * Allows to select the guild language. + */ +export class SelectLanguageCommand extends CustomCommand { + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['sl'], + description: languageKeys.commands.misc.selectlanguage.commandDescription, + preconditions: [CustomPrecondition.AdminOnly, CustomPrecondition.BotInitializedOnly], + runIn: [CommandOptionsRunTypeEnum.GuildAny], + }) + } + + async messageRun(message: Message, { t }: CustomArgs) { + const availableLanguages = new MessageActionRow().addComponents( + ...Object.entries(languagesTypes).map(([locale, properties]) => + getButton(locale, properties.language, 'PRIMARY', properties.emoji) + ) + ) + const selectLanguageMsg = await message.channel.send({ + content: t(languageKeys.commands.misc.selectlanguage.messageContent), + components: [availableLanguages], + }) + + const languageCollector = message.channel.createMessageComponentCollector({ + filter: (btnInteraction) => btnInteraction.user.id === message.author.id, + max: 1, + time: 1000 * 15, + }) + + languageCollector.on('collect', async (i: ButtonInteraction) => { + try { + const guild = await this.container.db.guildService.getById(message.guild.id) + guild.guildLanguage = i.customId + await guild.save() + const languageChangedMessage = await resolveKey( + message, + languageKeys.commands.misc.selectlanguage.languageChangedMessage, + { + language: i.component.label, + } + ) + await i.update({ + content: languageChangedMessage, + components: [], + }) + } catch (error) { + await i.update(t(languageKeys.errors.unexpectedError)) + } + }) + + languageCollector.on('end', async (collection) => { + if (!collection.first()) { + await selectLanguageMsg.edit({ + content: t(languageKeys.commands.misc.selectlanguage.ignoreMessage), + components: [], + }) + } + setTimeout(async () => { + await selectLanguageMsg.delete().catch() + }, 1000 * 7) + }) + } +} diff --git a/src/commands/misc/shameonyou.ts b/src/commands/misc/shameonyou.ts index 166885b..61c33a2 100644 --- a/src/commands/misc/shameonyou.ts +++ b/src/commands/misc/shameonyou.ts @@ -1,43 +1,43 @@ -import { Args, Command } from '@sapphire/framework' -import { Message, MessageEmbed } from 'discord.js' -import { configuration } from '@/config' -import { commandsLinks } from '@/utils' - -/** - * Sends an embed message disrespecting certain User and a disrespectful image. - */ -export class ShameOnYouCommand extends Command { - constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - name: 'shameonyou', - aliases: ['soy'], - fullCategory: ['misc'], - description: 'Disrespects some @User.', - runIn: ['GUILD_ANY'], - }) - } - - /** - * It executes when someone types the "shameonyou" command. - */ - async messageRun(message: Message, args: Args): Promise { - const disrespectedPerson = await args.pick('user') - - // Obtains disrespected gif's urls. - const { gifs } = commandsLinks.misc.shameonyou - const randIndex = Math.floor(Math.random() * gifs.length) - - const embedMessage = new MessageEmbed() - .setDescription(`Shame on you! ${disrespectedPerson.username} !!`) - .setColor(configuration.embedMessageColor) - .setImage(gifs[randIndex]) - - try { - await message.channel.send({ embeds: [embedMessage] }) - return message.delete() - } catch (error) { - // If bot cannot delete messages. - } - } -} +import { Command, CommandOptionsRunTypeEnum } from '@sapphire/framework' +import { Message, MessageEmbed } from 'discord.js' +import { configuration } from '@/config' +import { commandsLinks, languageKeys, CustomCommand, CustomArgs } from '@/utils' + +/** + * Sends an embed message disrespecting certain User and a disrespectful image. + */ +export class ShameOnYouCommand extends CustomCommand { + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['soy'], + description: languageKeys.commands.misc.shameonyou.description, + runIn: [CommandOptionsRunTypeEnum.GuildAny], + }) + } + + /** + * It executes when someone types the "shameonyou" command. + */ + async messageRun(message: Message, args: CustomArgs): Promise { + const disrespectedPerson = await args.pick('user') + + // Obtains disrespected gif's urls. + const { gifs } = commandsLinks.misc.shameonyou + const randIndex = Math.floor(Math.random() * gifs.length) + + const embedMessage = new MessageEmbed() + .setDescription( + args.t(languageKeys.commands.misc.shameonyou.embedMessageDescription, { username: disrespectedPerson.username }) + ) + .setColor(configuration.client.embedMessageColor) + .setImage(gifs[randIndex]) + + try { + await message.channel.send({ embeds: [embedMessage] }) + return message.delete() + } catch (error) { + // If bot cannot delete messages. + } + } +} diff --git a/src/commands/roles/activateRoles.ts b/src/commands/roles/activateroles.ts similarity index 56% rename from src/commands/roles/activateRoles.ts rename to src/commands/roles/activateroles.ts index 9421e5c..8249ab4 100644 --- a/src/commands/roles/activateRoles.ts +++ b/src/commands/roles/activateroles.ts @@ -1,10 +1,4 @@ -import { - CollectorFilter, - Message, - MessageActionRow, - MessageComponentInteraction, - MessageEmbed, -} from 'discord.js' +import { CollectorFilter, Message, MessageActionRow, MessageComponentInteraction, MessageEmbed } from 'discord.js' import { Command, container } from '@sapphire/framework' import { ActivateRolesResolverParams, @@ -16,6 +10,10 @@ import { roles, RolesButtonId, UserActions, + CustomCommand, + CustomArgs, + CustomPrecondition, + languageKeys, } from '@/utils' import { configuration } from '@/config' import { Guild } from '@/database' @@ -23,11 +21,8 @@ import { Guild } from '@/database' /** * Activate roles functionality. */ -export class ActivateRolesCommand extends Command { - private resolver: Record< - UserActions, - (_: ActivateRolesResolverParams) => Promise - > = { +export class ActivateRolesCommand extends CustomCommand { + private resolver: Record Promise> = { [UserActions.ACCEPT]: this.activate.bind(this), [UserActions.REJECT]: this.reject.bind(this), [UserActions.IGNORE]: this.ignore.bind(this), @@ -36,11 +31,13 @@ export class ActivateRolesCommand extends Command { constructor(context: Command.Context, options: Command.Options) { super(context, { ...options, - name: 'activateroles', aliases: ['ar'], - fullCategory: ['roles'], - description: 'Activates bot roles.', - preconditions: ['AdminOnly', 'BotInitializeOnly', 'RolesNotActiveOnly'], + description: languageKeys.commands.roles.activateroles.description, + preconditions: [ + CustomPrecondition.AdminOnly, + CustomPrecondition.BotInitializedOnly, + CustomPrecondition.RolesDeactivatedOnly, + ], requiredUserPermissions: ['ADMINISTRATOR'], requiredClientPermissions: ['MANAGE_ROLES'], }) @@ -49,7 +46,10 @@ export class ActivateRolesCommand extends Command { /** * It executes when someone types the "activateroles" command. */ - async messageRun(message: Message): Promise { + async messageRun(message: Message, { t }: CustomArgs): Promise { + const { rolesListText, messageFooter, acceptButtonLabel, rejectButtonLabel, unexpectedError } = + languageKeys.commands.roles.activateroles + try { let rolesList = '' const everyRole = Object.values(roles) @@ -57,21 +57,21 @@ export class ActivateRolesCommand extends Command { rolesList += `• ${role.name}\n` }) + const { appName } = configuration + const embedMessage = new MessageEmbed() .setAuthor({ name: configuration.appName, iconURL: botLogoURL, }) .setThumbnail(botLogoURL) - .addField('The next roles will be added to your server:', rolesList) - .setColor(configuration.embedMessageColor) - .setFooter( - `> Do you really want to activate ${configuration.appName}'s roles ?` - ) + .addField(t(rolesListText), rolesList) + .setColor(configuration.client.embedMessageColor) + .setFooter(t(messageFooter, { appName })) const buttons = new MessageActionRow().addComponents( - getButton(RolesButtonId.ACCEPT, 'ACCEPT', 'SUCCESS'), - getButton(RolesButtonId.REJECT, 'REJECT', 'DANGER') + getButton(RolesButtonId.ACCEPT, t(acceptButtonLabel), 'SUCCESS'), + getButton(RolesButtonId.REJECT, t(rejectButtonLabel), 'DANGER') ) const activateRolesEmbedMessage = await message.channel.send({ @@ -80,9 +80,8 @@ export class ActivateRolesCommand extends Command { components: [buttons], }) - const filter: CollectorFilter<[MessageComponentInteraction<'cached'>]> = ( - btnInteraction - ) => btnInteraction.user.id === message.author.id + const filter: CollectorFilter<[MessageComponentInteraction<'cached'>]> = (btnInteraction) => + btnInteraction.user.id === message.author.id // Waits 15 seconds for response. const collector = message.channel.createMessageComponentCollector({ @@ -101,6 +100,7 @@ export class ActivateRolesCommand extends Command { this.resolver[authorAction]({ message, + t, interaction: collection.first(), }).then(({ message, interaction }) => { activateRolesEmbedMessage.delete().catch() @@ -111,26 +111,20 @@ export class ActivateRolesCommand extends Command { }) }) } catch (error) { - logger.error( - `MongoDB Connection error. Could not initiate roles game for '${message.guild.name}' server`, - { context: this.constructor.name } - ) - return message.channel.send( - 'It occured an unexpected error, roles could not be created ): Try again later :sweat:' - ) + logger.error(`MongoDB Connection error. Could not initiate roles game for '${message.guild.name}' server`, { + context: this.constructor.name, + }) + return message.channel.send(t(unexpectedError)) } } - async activate({ message, interaction }: ActivateRolesResolverParams) { - const activatingRolesMsg = await message.channel.send( - `Okay, we're working for you, meanwhile take a nap n.n` - ) + async activate({ message, t, interaction }: ActivateRolesResolverParams) { + const { activateMessage, rolesCreationError, rolesCreationSuccessful } = languageKeys.commands.roles.activateroles + const activatingRolesMsg = await message.channel.send(t(activateMessage)) const rolesCreated = await initRoles(message) if (!rolesCreated) { - interaction.reply( - `Error while trying to create roles, maybe I don't have enough permissions :sweat:` - ) + interaction.reply(rolesCreationError) return { interaction } } @@ -145,21 +139,17 @@ export class ActivateRolesCommand extends Command { } container.cache.set(message.guild.id, newCachedGuild) - interaction.reply( - `Roles created successfully :purple_heart: Try to see yours with **${configuration.prefix}myroles** command.` - ) + interaction.reply(t(rolesCreationSuccessful, { prefix: configuration.client.defaultPrefix })) return { message: activatingRolesMsg, interaction } } - async reject({ interaction }: ActivateRolesResolverParams) { - await interaction.reply(`Okay! we're not gonna create any role (:`) + async reject({ t, interaction }: ActivateRolesResolverParams) { + await interaction.reply(t(languageKeys.commands.roles.activateroles.rejectMessage)) return { interaction } } - async ignore({ message }: ActivateRolesResolverParams) { - const timesUpMessage = await message.channel.send( - `Time's up! Try again later ):` - ) + async ignore({ message, t }: ActivateRolesResolverParams) { + const timesUpMessage = await message.channel.send(t(languageKeys.commands.roles.activateroles.ignoreMessage)) return { message: timesUpMessage } } } diff --git a/src/commands/roles/myRole.ts b/src/commands/roles/myRole.ts deleted file mode 100644 index b580fc8..0000000 --- a/src/commands/roles/myRole.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Message, MessageEmbed } from 'discord.js' -import { Command, container } from '@sapphire/framework' -import { configuration } from '@/config' -import { - getNextAvailableRoleFromUser, - getRole, - getRoleFromUser, - roles, - utilLinks, -} from '@/utils' - -/** - * Displays information about the role and score of an specific User. - */ -export class MyRoleCommand extends Command { - constructor(context: Command.Context, options: Command.Options) { - super(context, { - ...options, - name: 'myrole', - aliases: ['mr'], - fullCategory: ['roles'], - description: `Shows user's role and their score.`, - preconditions: ['RolesActiveOnly'], - runIn: ['GUILD_ANY'], - }) - } - - /** - * It executes when someone types the "roles" command. - */ - async messageRun(message: Message): Promise { - const { id: guildId } = message.guild - const { - author: { id }, - } = message - const user = message.guild.members.cache.find((member) => member.id === id) - - let score = 'Who knows D:' - try { - const user = await container.db.userService.getById(id) - const guildData = user.guildsData.find( - (guildData) => guildData.guildId === guildId - ) - score = guildData.participationScore.toString() - } catch (error) {} - - const embedMessage = new MessageEmbed() - .setColor(configuration.embedMessageColor) - .setDescription(`${message.author.username}'s Role`) - - const discordRole = getRoleFromUser(user) - const currentBotRelatedRole = getRole(discordRole) - const nextAvailableRole = getNextAvailableRoleFromUser(user) - if ( - (nextAvailableRole && nextAvailableRole.name !== roles.zote.name) || - !nextAvailableRole - ) { - embedMessage.addField( - 'You have the following role:', - `• ${currentBotRelatedRole.name}` - ) - embedMessage.setImage(currentBotRelatedRole.imageUrl) - } else { - embedMessage.addField( - `You don't have any role`, - 'Try to be more participatory n.n' - ) - embedMessage.setImage(utilLinks.roles.noRole) - } - - embedMessage.addField('Current Score', score) - if (nextAvailableRole) { - embedMessage.setFooter( - `You need ${ - nextAvailableRole.requiredPoints - parseInt(score) - } more points to get '${nextAvailableRole.name}' role.` - ) - } else { - embedMessage.setFooter( - `You are ${currentBotRelatedRole.name}, you have reached the absolute supremacy!!` - ) - } - - return message.channel.send({ embeds: [embedMessage] }) - } -} diff --git a/src/commands/roles/myrole.ts b/src/commands/roles/myrole.ts new file mode 100644 index 0000000..e228e59 --- /dev/null +++ b/src/commands/roles/myrole.ts @@ -0,0 +1,93 @@ +import { Message, MessageEmbed } from 'discord.js' +import { Command, CommandOptionsRunTypeEnum, container } from '@sapphire/framework' +import { configuration } from '@/config' +import { + getNextAvailableRoleFromUser, + getRole, + getRoleFromUser, + roles, + utilLinks, + CustomPrecondition, + languageKeys, + CustomCommand, + CustomArgs, +} from '@/utils' + +/** + * Displays information about the role and score of an specific User. + */ +export class MyRoleCommand extends CustomCommand { + constructor(context: Command.Context, options: Command.Options) { + super(context, { + ...options, + aliases: ['mr'], + description: languageKeys.commands.roles.myrole.description, + preconditions: [CustomPrecondition.RolesActivatedOnly], + runIn: [CommandOptionsRunTypeEnum.GuildAny], + }) + } + + /** + * It executes when someone types the "roles" command. + */ + async messageRun(message: Message, { t }: CustomArgs): Promise { + const { id: guildId } = message.guild + const { + author: { id }, + } = message + const user = message.guild.members.cache.find((member) => member.id === id) + + let score: number + try { + const user = await container.db.userService.getById(id) + const guildData = user.guildsData.find((guildData) => guildData.guildId === guildId) + score = guildData.participationScore + } catch (error) {} + + const { + embedMessageDescription, + followingRoleTitle, + noHaveRoleTitle, + noHaveRoleText, + currentScoreTitle, + currentScoreText, + undefinedScoreText, + messageFooter, + messageFooterMaxPoints, + } = languageKeys.commands.roles.myrole + + const embedMessage = new MessageEmbed() + .setColor(configuration.client.embedMessageColor) + .setDescription(t(embedMessageDescription, { username: message.author.username })) + + const discordRole = getRoleFromUser(user) + const currentBotRelatedRole = getRole(discordRole) + const nextAvailableRole = getNextAvailableRoleFromUser(user) + if ((nextAvailableRole && nextAvailableRole.name !== roles.zote.name) || !nextAvailableRole) { + embedMessage.addField(t(followingRoleTitle), `• ${currentBotRelatedRole.name}`) + embedMessage.setImage(currentBotRelatedRole.imageUrl) + } else { + embedMessage.addField(t(noHaveRoleTitle), t(noHaveRoleText)) + embedMessage.setImage(utilLinks.roles.noRole) + } + + const hasScore = score && score >= 0 + + embedMessage.addField( + t(currentScoreTitle), + t(currentScoreText, { points: hasScore ? score.toString() : t(undefinedScoreText) }) + ) + if (nextAvailableRole) { + embedMessage.setFooter( + t(messageFooter, { + points: hasScore ? nextAvailableRole.requiredPoints - score : 'some', + role: nextAvailableRole.name, + }) + ) + } else { + embedMessage.setFooter(t(messageFooterMaxPoints, { maxRole: currentBotRelatedRole.name })) + } + + return message.channel.send({ embeds: [embedMessage] }) + } +} diff --git a/src/commands/roles/roles.ts b/src/commands/roles/roles.ts index e6cea2a..3283264 100644 --- a/src/commands/roles/roles.ts +++ b/src/commands/roles/roles.ts @@ -1,63 +1,68 @@ import { Message, MessageEmbed } from 'discord.js' -import { Args, Command, container } from '@sapphire/framework' +import { Command, CommandOptionsRunTypeEnum, container } from '@sapphire/framework' import { configuration } from '@/config' -import { getRoleFromUser, roles } from '@/utils' +import { getRoleFromUser, roles, CustomPrecondition, CustomCommand, CustomArgs, languageKeys } from '@/utils' /** * Displays information about roles and their respective scores. */ -export class RolesCommand extends Command { +export class RolesCommand extends CustomCommand { constructor(context: Command.Context, options: Command.Options) { super(context, { ...options, - name: 'roles', aliases: ['r'], - fullCategory: ['roles'], - description: `Shows every registered ${configuration.appName}'s roles or specific @User's role.`, - preconditions: ['RolesActiveOnly'], - runIn: ['GUILD_ANY'], + description: languageKeys.commands.roles.roles.description, + preconditions: [CustomPrecondition.RolesActivatedOnly], + runIn: [CommandOptionsRunTypeEnum.GuildAny], }) } /** * It executes when someone types the "roles" command. */ - async messageRun(message: Message, args: Args): Promise { + async messageRun(message: Message, args: CustomArgs): Promise { const { id: guildId } = message.guild - const embedMessage = new MessageEmbed().setColor( - configuration.embedMessageColor - ) + const embedMessage = new MessageEmbed().setColor(configuration.client.embedMessageColor) const user = await args.pick('user').catch(() => null) if (!!user && !user.bot) { const guildUser = await message.guild.members.fetch(user.id) const currentRole = getRoleFromUser(guildUser) + const { + currentRoleTitle, + noCurrentRoleText, + userRolesMessageDescription, + currentScoreTitle, + currentScoreText, + undefinedScoreText, + } = languageKeys.commands.roles.roles + if (currentRole) { - embedMessage.addField('Current Role', currentRole.name) + embedMessage.addField(args.t(currentRoleTitle), currentRole.name) } else { - embedMessage.addField('Current Role', 'None') + embedMessage.addField(args.t(currentRoleTitle), args.t(noCurrentRoleText)) } - embedMessage.setDescription( - `:jack_o_lantern: ${user.username} :jack_o_lantern:` - ) + embedMessage.setDescription(args.t(userRolesMessageDescription, { username: user.username })) - let score = 'Who knows D:' + let score: number try { const userDb = await container.db.userService.getById(user.id) - const guildData = userDb.guildsData.find( - (guildData) => guildData.guildId === guildId - ) - score = guildData.participationScore.toString() + const guildData = userDb.guildsData.find((guildData) => guildData.guildId === guildId) + score = guildData.participationScore } catch (error) {} - embedMessage.addField('Current Score', score) - } else { - embedMessage.setDescription( - `:jack_o_lantern: Available ${configuration.appName}'s Roles :jack_o_lantern:` + const hasScore = score && score >= 0 + embedMessage.addField( + args.t(currentScoreTitle), + args.t(currentScoreText, { points: hasScore ? score.toString() : args.t(undefinedScoreText) }) ) + } else { + const { everyRoleMessageDescription, rolesTitle, requiredScoresTitle, messageFooter } = + languageKeys.commands.roles.roles + embedMessage.setDescription(args.t(everyRoleMessageDescription, { appName: configuration.appName })) let rolesList = '' let scoresList = '' @@ -67,11 +72,9 @@ export class RolesCommand extends Command { scoresList += `${role.requiredPoints} \n` }) - embedMessage.addField('Roles', rolesList, true) - embedMessage.addField('Required scores', scoresList, true) - embedMessage.setFooter( - 'You can increase your score being participatory and interacting with other users n.n' - ) + embedMessage.addField(args.t(rolesTitle), rolesList, true) + embedMessage.addField(args.t(requiredScoresTitle), scoresList, true) + embedMessage.setFooter(args.t(messageFooter)) } return message.channel.send({ embeds: [embedMessage] }) diff --git a/src/config/index.ts b/src/config/index.ts index 9890304..451ad47 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,17 +1,19 @@ -import { ColorResolvable } from 'discord.js' -import { config } from 'dotenv' - -config() - -export const configuration = { - env: process.env.NODE_ENV || 'development', - appName: process.env.APP_NAME || 'Chibi Knight', - clientId: process.env.CLIENT_ID, - token: process.env.BOT_TOKEN, - prefix: process.env.BOT_PREFIX, - embedMessageColor: (process.env.EMBED_MESSAGE_COLOR || - '#57a7ef') as ColorResolvable, - mongodb: { - connection_url: process.env.MONGODB_CONNECTION, - }, -} +import { ColorResolvable } from 'discord.js' +import { config } from 'dotenv' + +config() + +export const configuration = { + env: process.env.NODE_ENV || 'development', + appName: process.env.APP_NAME || 'Chibi Knight', + client: { + id: process.env.CLIENT_ID, + token: process.env.BOT_TOKEN, + defaultPrefix: process.env.BOT_PREFIX, + embedMessageColor: (process.env.EMBED_MESSAGE_COLOR || '#57a7ef') as ColorResolvable, + loadDefaultErrorListeners: process.env.NODE_ENV === 'development', + }, + mongodb: { + connection_url: process.env.MONGODB_CONNECTION, + }, +} diff --git a/src/database/cache.ts b/src/database/cache.ts index a998fbc..14fb14c 100644 --- a/src/database/cache.ts +++ b/src/database/cache.ts @@ -31,12 +31,9 @@ export class Cache { this.instance.set(guildId, cachedGuild) }) } catch (error) { - logger.error( - `MongoDB Connection error. Could not init cache from database`, - { - context: this.constructor.name, - } - ) + logger.error(`MongoDB Connection error. Could not init cache from database`, { + context: this.constructor.name, + }) } return this.instance } diff --git a/src/database/database.ts b/src/database/database.ts index de649ab..c250f0e 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -19,12 +19,9 @@ export class MongoDatabase { return this.instance } - const connection = await mongoose.connect( - configuration.mongodb.connection_url, - { - serverSelectionTimeoutMS: 4000, - } - ) + const connection = await mongoose.connect(configuration.mongodb.connection_url, { + serverSelectionTimeoutMS: 4000, + }) return new MongoDatabase(connection) } diff --git a/src/database/models/guild.model.ts b/src/database/models/guild.model.ts index b299810..fde9e81 100644 --- a/src/database/models/guild.model.ts +++ b/src/database/models/guild.model.ts @@ -18,4 +18,10 @@ export class Guild { default: false, }) public gameInstanceActive?: boolean + + @prop({ + type: String, + default: 'en-US', + }) + public guildLanguage?: string } diff --git a/src/database/services/guild.service.ts b/src/database/services/guild.service.ts index d499c3c..08c9454 100644 --- a/src/database/services/guild.service.ts +++ b/src/database/services/guild.service.ts @@ -1,8 +1,4 @@ -import { - DocumentType, - ReturnModelType, - getModelForClass, -} from '@typegoose/typegoose' +import { DocumentType, ReturnModelType, getModelForClass } from '@typegoose/typegoose' import { Guild } from '../models' export class GuildService { diff --git a/src/database/services/user.service.ts b/src/database/services/user.service.ts index 6690fe6..ed1c8d6 100644 --- a/src/database/services/user.service.ts +++ b/src/database/services/user.service.ts @@ -1,8 +1,4 @@ -import { - DocumentType, - ReturnModelType, - getModelForClass, -} from '@typegoose/typegoose' +import { DocumentType, ReturnModelType, getModelForClass } from '@typegoose/typegoose' import { Aggregate } from 'mongoose' import { User } from '../models' @@ -21,20 +17,11 @@ export class UserService { return this.userRepository.find().exec() } - async getByFilter( - filter: any, - limit = 10, - sort = 1 - ): Promise[]> { + async getByFilter(filter: any, limit = 10, sort = 1): Promise[]> { return this.userRepository.find(filter).limit(limit).sort(sort).exec() } - async getByNestedFilter( - unwind: string, - filter: any, - sort: any, - limit = 10 - ): Promise> { + async getByNestedFilter(unwind: string, filter: any, sort: any, limit = 10): Promise> { return await this.userRepository.aggregate([ { $unwind: `$${unwind}` }, { $match: filter }, diff --git a/src/languages/en-US/categories.json b/src/languages/en-US/categories.json new file mode 100644 index 0000000..6364a9f --- /dev/null +++ b/src/languages/en-US/categories.json @@ -0,0 +1,5 @@ +{ + "gamesCategoryDecoratedTitle": ":game_die: GAMES :game_die:", + "miscCategoryDecoratedTitle": ":tickets: MISCELLANEOUS :tickets:", + "rolesCategoryDecoratedTitle": ":jack_o_lantern: ROLES :jack_o_lantern:" +} \ No newline at end of file diff --git a/src/languages/en-US/commands/games.json b/src/languages/en-US/commands/games.json new file mode 100644 index 0000000..4919612 --- /dev/null +++ b/src/languages/en-US/commands/games.json @@ -0,0 +1,42 @@ +{ + "cancelgameDescription": "Cancels the active game.", + "cancelgameNoActiveGame": "There's no active game.", + "cancelgameGameCancelled": "Game cancelled.", + + "tictactoeDescription": "Initiates a tictactoe game.", + "tictactoeActiveGameInstance": "There's already a game in course {{username}}!", + "tictactoeChallengeYourself": "You cannot challenge yourself ¬¬ ...", + "tictactoeChallengeBot": "You cannot challenge me :anger: I'm a superior being... I would destroy you n.n :purple_heart:", + "tictactoeAcceptChallengeButtonLabel": "YES", + "tictactoeRejectChallengeButtonLabel": "NO", + "tictactoeStartGameQuestion": "`{{player2Username}}` has been challenged by `{{player1Username}}` to play #. Do you accept the challenge?", + "tictactoeRejectChallengeMessage": "Game cancelled ): Come back when you are brave enough, {{username}}.", + "tictactoeIgnoreChallengeMessage": "Time's up! {{player2Username}} doesn't want to play ):", + "tictactoeErrorMessage": "Error ): could not start game. Try again later :purple_heart:", + "tictactoeInstructionsTitle": "Instructions", + "tictactoeInstructionsText": "Select the number where you want to make your move.", + "tictactoeCurrentTurnLabel": "Current Turn", + "tictactoeCurrentTurnText": "It's your turn, {{activePlayerUsername}}", + "tictactoeResultText": "Result :trophy:", + "tictactoeConsolationPrizeTitleOneLoser": "Consolation prize :second_place:", + "tictactoeConsolationPrizeTextOneLoser": "Don't worry {{loserUsername}}, this is for you:", + "tictactoeConsolationPrizeTitleTwoLosers": "Consolation prize :woozy_face:", + "tictactoeConsolationPrizeTextTwoLosers": "You two are so bad, this is for you <3", + "tictactoeGameTitle": ":x::o: Tic Tac Toe :x::o:", + "tictactoeGameDescription": "Beat your friends just taking advantage of their inferior mental capabilities, oh, and using :x::x::o::o:", + "tictactoeChallengersTitle": "Challengers", + "tictactoeChallengersText": "{{player1Username}} V/S {{player2Username}}", + "tictactoeBoardTitle": "Board", + "tictactoePositionsReferenceTitle": "Positions reference", + "tictactoeVictoryResult": ":tada: CONGRATULATIONS {{username}}! You have won! :tada:", + "tictactoeTieResult": "The game was a tie! :confetti_ball: Thanks for play (:", + + "tictactoeleaderboardDescription": "Displays the tictactoe leaderboard.", + "tictactoeleaderboardMessageTitle": ":x::o: TicTacToe Leaderboard :x::o:", + "tictactoeleaderboardMessageDescription": "Scoreboard of TicTacToe based on victories", + "tictactoeleaderboardPositioningLabel": "Positioning", + "tictactoeleaderboardVictoriesLabel": "Victories", + "tictactoeleaderboardErrorMessage": "Sorry ): I couldn't retrieve tictactoe leaderboard. I failed you :sweat:", + + "cancelGameButtonLabel": "CANCEL GAME" +} \ No newline at end of file diff --git a/src/languages/en-US/commands/misc.json b/src/languages/en-US/commands/misc.json new file mode 100644 index 0000000..279ce49 --- /dev/null +++ b/src/languages/en-US/commands/misc.json @@ -0,0 +1,24 @@ +{ + "congratulateDescription": "Congratulates some @User.", + "congratulateEmbedMessageDescription": "Congratulations, {{username}} !!", + + "helpDescription": "Gives information about every existing command.", + "helpUnknownCommandError": "Unknown command!! You could check the available commands with `{{prefix}}help`", + "helpEveryCommandEmbedMessageDescription": ":crossed_swords: These are the available commands for {{appName}} n.n", + "helpEveryCommandEmbedMessageFooter": "Type {{prefix}}help {command} to see information about an specific command.", + "helpCommandWithoutDescription": "No description was provided.", + + "initDescription": "Initialize Chibi Knight funcionalities.", + "initSuccessfullyEvent": "{{appName}} has been initialize successfully :purple_heart: check out the commands with **{{prefix}}help** :smile:", + "initFailureEvent": "It occured an unexpected error while trying to initialize {{appName}} :sweat: try again later.", + + "sayDescription": "Replies with the received message.", + + "selectLanguageCommandDescription": "Allows to select the language that {{appName}} will use.", + "selectLanguageMessageContent": "Select the language:", + "selectLanguageIgnoreMessage": "Time's up! the server language remains the same.", + "selectLanguage_LanguageChangedMessage": "Now the language is **{{language}}**", + + "shameonyouDescription": "Disrespects some @User.", + "shameonyouEmbedMessageDescription": "Shame on you, {{username}}!!" +} \ No newline at end of file diff --git a/src/languages/en-US/commands/roles.json b/src/languages/en-US/commands/roles.json new file mode 100644 index 0000000..86312b4 --- /dev/null +++ b/src/languages/en-US/commands/roles.json @@ -0,0 +1,37 @@ +{ + "activaterolesCommandDescription": "Activates bot roles.", + "activaterolesRolesListText": "The next roles will be added to your server:", + "activaterolesMessageFooter": "> Do you really want to activate {{appName}}'s roles ?", + "activaterolesAcceptButtonLabel": "ACCEPT", + "activaterolesRejectButtonLabel": "REJECT", + "activaterolesUnexpectedError": "It occured an unexpected error, roles could not be created ): Try again later :sweat:", + "activaterolesActivateMessage": "Okay, we're working for you, meanwhile just relax n.n", + "activaterolesRolesCreationError": "Error while trying to create roles, maybe I don't have enough permissions :sweat:", + "activaterolesRolesCreationSuccessful": "Roles created successfully :purple_heart: See yours with **{{prefix}}myroles** command.", + "activaterolesRejectMessage": "Okay! I'am not gonna create any role (:", + "activaterolesIgnoreMessage": "Time's up! roles won't be created", + + "myroleCommandDescription": "Shows your role and your current score.", + "myroleEmbedMessageDescription": "{{username}}'s Role", + "myroleFollowingRoleTitle": "You have the following role:", + "myroleHaveNoRoleTitle": "You don't have any role", + "myroleHaveNoRoleText": "Try to be more participatory n.n", + "myroleCurrentScoreTitle": "Current Score", + "myroleCurrentScoreText": "{{points}} points", + "myroleUndefinedScoreText": "some", + "myroleMessageFooter": "You need {{points}} more points to get '{{role}}' role.", + "myroleMessageFooterMaxPoints": "You are {{maxRole}}, you have reached the absolute supremacy!!", + + + "rolesCommandDescription": "Shows every registered role or specific @User's role.", + "rolesCurrentRoleTitle": "Current Role", + "rolesNoCurrentRoleText": "None", + "rolesUserRolesMessageDescription": ":jack_o_lantern: {{username}} :jack_o_lantern:", + "rolesCurrentScoreTitle": "Current Score", + "rolesCurrentScoreText": "{{points}} points", + "rolesUndefinedScoreText": "some", + "rolesEveryRoleMessageDescription": ":jack_o_lantern: Available {{appName}}'s Roles :jack_o_lantern:", + "rolesRolesTitle": "Roles", + "rolesRequiredScoresTitle": "Required scores", + "rolesMessageFooter": "You can increase your score being participatory and interacting with other users n.n" +} \ No newline at end of file diff --git a/src/languages/en-US/errors.json b/src/languages/en-US/errors.json new file mode 100644 index 0000000..63e6f31 --- /dev/null +++ b/src/languages/en-US/errors.json @@ -0,0 +1,3 @@ +{ + "unexpectedError": "It occured an unexpected error :sweat: try again later." +} \ No newline at end of file diff --git a/src/languages/en-US/listeners/commands.json b/src/languages/en-US/listeners/commands.json new file mode 100644 index 0000000..96891ca --- /dev/null +++ b/src/languages/en-US/listeners/commands.json @@ -0,0 +1,6 @@ +{ + "argumentErrorMessage": "That argument is not valid!", + "userErrorMessage": "You need to write more parameters!", + "fallbackErrorMessage": "You need to write more parameters!", + "helperMessageExtension": "\n> **Tip**: You can do `{{prefix}}help {{commandName}}` to find out how to use this command." +} \ No newline at end of file diff --git a/src/languages/en-US/listeners/guild.json b/src/languages/en-US/listeners/guild.json new file mode 100644 index 0000000..9542ba6 --- /dev/null +++ b/src/languages/en-US/listeners/guild.json @@ -0,0 +1,3 @@ +{ + "guildCreateWelcomeMessage": "Thanks for invite me to your server n.n please, first run the **{{prefix}}init** command, I need it to work correctly (:" +} \ No newline at end of file diff --git a/src/languages/en-US/preconditions/roles.json b/src/languages/en-US/preconditions/roles.json new file mode 100644 index 0000000..6bed0f2 --- /dev/null +++ b/src/languages/en-US/preconditions/roles.json @@ -0,0 +1,4 @@ +{ + "rolesActiveOnly_deactivatedRolesError": "{{appName}}'s roles are not activated. First, you have to run `{{prefix}}activateroles`", + "rolesNotActiveOnly_activatedRolesError": "You already have initialized {{appName}}'s roles :relieved: Check yours with **{{prefix}}myroles**." +} \ No newline at end of file diff --git a/src/languages/en-US/preconditions/server.json b/src/languages/en-US/preconditions/server.json new file mode 100644 index 0000000..41a25e3 --- /dev/null +++ b/src/languages/en-US/preconditions/server.json @@ -0,0 +1,4 @@ +{ + "botInitializedOnly_errorMessage": "First you have to initialize me running **{{prefix}}init** command.", + "botNotInitializeOnly_errorMessage": "{{appName}} has already been initialized n.n" +} \ No newline at end of file diff --git a/src/languages/en-US/preconditions/user.json b/src/languages/en-US/preconditions/user.json new file mode 100644 index 0000000..0753db3 --- /dev/null +++ b/src/languages/en-US/preconditions/user.json @@ -0,0 +1,3 @@ +{ + "adminOnly_errorMessage": "You don't have permissions to run this command. Contact with an Administrator :sweat:" +} \ No newline at end of file diff --git a/src/languages/en-US/rolesAssignment.json b/src/languages/en-US/rolesAssignment.json new file mode 100644 index 0000000..f90d6aa --- /dev/null +++ b/src/languages/en-US/rolesAssignment.json @@ -0,0 +1,4 @@ +{ + "userObtainsNewRole": "Congratulations {{username}}, you have obtain the '{{roleName}}' role!", + "newRoleAssignmentError": "Failed '{{roleName}}' role assignation. I guess I need more permissions ):" +} \ No newline at end of file diff --git a/src/languages/es-ES/categories.json b/src/languages/es-ES/categories.json new file mode 100644 index 0000000..b0ea04e --- /dev/null +++ b/src/languages/es-ES/categories.json @@ -0,0 +1,5 @@ +{ + "gamesCategoryDecoratedTitle": ":game_die: JUEGOS :game_die:", + "miscCategoryDecoratedTitle": ":tickets: MISCELÁNEO :tickets:", + "rolesCategoryDecoratedTitle": ":jack_o_lantern: ROLES :jack_o_lantern:" +} \ No newline at end of file diff --git a/src/languages/es-ES/commands/games.json b/src/languages/es-ES/commands/games.json new file mode 100644 index 0000000..4dac788 --- /dev/null +++ b/src/languages/es-ES/commands/games.json @@ -0,0 +1,42 @@ +{ + "cancelgameDescription": "Cancela el juego activo.", + "cancelgameNoActiveGame": "No hay ningún juego activo.", + "cancelgameGameCancelled": "Juego cancelado.", + + "tictactoeDescription": "Inicia un juego de _Tres en Raya_.", + "tictactoeActiveGameInstance": "¡Ya hay un juego en curso, {{username}}!", + "tictactoeChallengeYourself": "No puedes desafiarte a ti mismo ¬¬ ...", + "tictactoeChallengeBot": "No puedes desafiarme :anger: soy un ser superior... te destruiría n.n :purple_heart:", + "tictactoeAcceptChallengeButtonLabel": "SÍ", + "tictactoeRejectChallengeButtonLabel": "NO", + "tictactoeStartGameQuestion": "`{{player2Username}}` ha sido desafiado por `{{player1Username}}` a jugar #. ¿Aceptas el desafío?", + "tictactoeRejectChallengeMessage": "Juego cancelado ): vuelve cuando seas lo suficientemente valiente, {{username}}.", + "tictactoeIgnoreChallengeMessage": "¡Se acabó el tiempo! {{player2Username}} no quiere jugar ):", + "tictactoeErrorMessage": "Error ): no se pudo iniciar el juego. Inténtalo más tarde :purple_heart:", + "tictactoeInstructionsTitle": "Instrucciones", + "tictactoeInstructionsText": "Elige el número donde quieras hacer tu movimiento.", + "tictactoeCurrentTurnLabel": "Turno actual", + "tictactoeCurrentTurnText": "Es tu turno, {{activePlayerUsername}}", + "tictactoeResultText": "Resultado :trophy:", + "tictactoeConsolationPrizeTitleOneLoser": "Premio de consolación :second_place:", + "tictactoeConsolationPrizeTextOneLoser": "No te preocupes, {{loserUsername}}, esto es para ti:", + "tictactoeConsolationPrizeTitleTwoLosers": "Premio de consolación :woozy_face:", + "tictactoeConsolationPrizeTextTwoLosers": "Ambos son terribles, esto es para ustedes <3", + "tictactoeGameTitle": ":x::o: Tres en Raya :x::o:", + "tictactoeGameDescription": "Derrota a tus amigos tomando ventaja de su inferioridad mental, oh, y usando :x::x::o::o:", + "tictactoeChallengersTitle": "Retadores", + "tictactoeChallengersText": "{{player1Username}} CONTRA {{player2Username}}", + "tictactoeBoardTitle": "Tablero", + "tictactoePositionsReferenceTitle": "Referencia de posiciones", + "tictactoeVictoryResult": ":tada: ¡FELICIDADES, {{username}}! ¡Has ganado! :tada:", + "tictactoeTieResult": "¡Empate! :confetti_ball: gracias por jugar (:", + + "tictactoeleaderboardDescription": "Muestra la tabla de posiciones del juego _Tres en Raya_.", + "tictactoeleaderboardMessageTitle": ":x::o: Tabla de posiciones - _Tres en Raya_ :x::o:", + "tictactoeleaderboardMessageDescription": "Tabla de puntajes de _Tres en Raya_, basado en el número de victorias", + "tictactoeleaderboardPositioningLabel": "Posicionamiento", + "tictactoeleaderboardVictoriesLabel": "Victorias", + "tictactoeleaderboardErrorMessage": "Lo siento ): No pude obtener la tabla de resultados. Te fallé :sweat:", + + "cancelGameButtonLabel": "CANCELAR JUEGO" +} \ No newline at end of file diff --git a/src/languages/es-ES/commands/misc.json b/src/languages/es-ES/commands/misc.json new file mode 100644 index 0000000..8c6238f --- /dev/null +++ b/src/languages/es-ES/commands/misc.json @@ -0,0 +1,24 @@ +{ + "congratulateDescription": "Felicita a algún @Usuario.", + "congratulateEmbedMessageDescription": "¡¡Felicidades, {{username}}!!", + + "helpDescription": "Muestra información sobre cada comando existente.", + "helpUnknownCommandError": "¡Comando desconocido! puedes revisar los comandos disponibles con `{{prefix}}help`", + "helpEveryCommandEmbedMessageDescription": ":crossed_swords: Estos son los comandos disponibles en {{appName}} n.n", + "helpEveryCommandEmbedMessageFooter": "Ejecuta {{prefix}}help {command} para ver información sobre un comando en específico.", + "helpCommandWithoutDescription": "Descripción no encontrada", + + "initDescription": "Inicializa las funcionalidades del bot.", + "initSuccessfullyEvent": "{{appName}} ha sido inicializado satisfactoriamente :purple_heart: revisa los comandos con `{{prefix}}help` :smile:", + "initFailureEvent": "Ocurrió un error inesperado durante la inicialización de {{appName}} :sweat: inténtalo más tarde.", + + "sayDescription": "Dice el mensaje recibido.", + + "selectLanguageCommandDescription": "Permite seleccionar el lenguaje que {{appName}} utilizará.", + "selectLanguageMessageContent": "Selecciona el idioma:", + "selectLanguageIgnoreMessage": "¡Se acabó el tiempo! el idioma del servidor seguirá siendo el mismo.", + "selectLanguage_LanguageChangedMessage": "Ahora el idioma es **{{language}}**.", + + "shameonyouDescription": "Falta el respeto a un @Usuario.", + "shameonyouEmbedMessageDescription": "¡Das lástima y verguenza, {{username}}!" +} \ No newline at end of file diff --git a/src/languages/es-ES/commands/roles.json b/src/languages/es-ES/commands/roles.json new file mode 100644 index 0000000..4868f49 --- /dev/null +++ b/src/languages/es-ES/commands/roles.json @@ -0,0 +1,36 @@ +{ + "activaterolesCommandDescription": "Activa los roles en el servidor.", + "activaterolesRolesListText": "Los siguientes roles serán agregados a tu servidor:", + "activaterolesMessageFooter": "> ¿Realmente quieres activar los roles de {{appName}}?", + "activaterolesAcceptButtonLabel": "ACEPTAR", + "activaterolesRejectButtonLabel": "RECHAZAR", + "activaterolesUnexpectedError": "Ocurrió un error inesperado, los roles no pudieron crearse ): inténtalo más tarde :sweat:", + "activaterolesActivateMessage": "Vale, estamos trabajando para ti, por mientras relájate n.n", + "activaterolesRolesCreationError": "Error mientras se intentaba crear los roles, tal vez no tengo suficientes permisos :sweat:", + "activaterolesRolesCreationSuccessful": "Roles creados satisfactoriamente :purple_heart: Ve los tuyos con el comando **{{prefix}}myroles**.", + "activaterolesRejectMessage": "¡Vale! no crearé ningún rol (:", + "activaterolesIgnoreMessage": "¡Se acabó el tiempo! no se creará ningún rol.", + + "myroleCommandDescription": "Muestra tu rol y tu puntaje actual.", + "myroleEmbedMessageDescription": "Rol de {{username}}", + "myroleFollowingRoleTitle": "Tienes el siguente rol:", + "myroleHaveNoRoleTitle": "No tienes ningún rol", + "myroleHaveNoRoleText": "Intenta ser más participativo n.n", + "myroleCurrentScoreTitle": "Puntaje actual", + "myroleCurrentScoreText": "{{points}} puntos", + "myroleUndefinedScoreText": "algunos", + "myroleMessageFooter": "Necesitas {{points}} puntos más para obtener el rol '{{role}}'.", + "myroleMessageFooterMaxPoints": "¡¡Eres {{maxRole}}, has alcanzado la supremacía absoluta!!", + + "rolesCommandDescription": "Muestra todos los roles existentes o el rol específico de un @Usuario.", + "rolesCurrentRoleTitle": "Rol actual", + "rolesNoCurrentRoleText": "Ninguno", + "rolesUserRolesMessageDescription": ":jack_o_lantern: {{username}} :jack_o_lantern:", + "rolesCurrentScoreTitle": "Puntaje actual", + "rolesCurrentScoreText": "{{points}} puntos", + "rolesUndefinedScoreText": "algunos", + "rolesEveryRoleMessageDescription": ":jack_o_lantern: Roles disponibles en {{appName}} :jack_o_lantern:", + "rolesRolesTitle": "Roles", + "rolesRequiredScoresTitle": "Puntajes requeridos", + "rolesMessageFooter": "Puedes incrementar tu puntaje siendo participativo e interactuando con otros usuarios n.n" +} \ No newline at end of file diff --git a/src/languages/es-ES/errors.json b/src/languages/es-ES/errors.json new file mode 100644 index 0000000..bad0f01 --- /dev/null +++ b/src/languages/es-ES/errors.json @@ -0,0 +1,3 @@ +{ + "unexpectedError": "Ocurrió un error inesperado :sweat: intenta más tarde." +} \ No newline at end of file diff --git a/src/languages/es-ES/listeners/commands.json b/src/languages/es-ES/listeners/commands.json new file mode 100644 index 0000000..def87f1 --- /dev/null +++ b/src/languages/es-ES/listeners/commands.json @@ -0,0 +1,6 @@ +{ + "argumentErrorMessage": "¡Ese argumento es inválido!", + "userErrorMessage": "¡Necesitas escribir más parámetros!", + "fallbackErrorMessage": "¡Necesitas escribir más parámetros!", + "helperMessageExtension": "\n> **Consejo**: Puedes ejecutar `{{prefix}}help {{commandName}}` para aprender cómo usar este comando." +} \ No newline at end of file diff --git a/src/languages/es-ES/listeners/guild.json b/src/languages/es-ES/listeners/guild.json new file mode 100644 index 0000000..0595959 --- /dev/null +++ b/src/languages/es-ES/listeners/guild.json @@ -0,0 +1,3 @@ +{ + "guildCreateWelcomeMessage": "Gracias por invitarme a tu server n.n por favor, primero ejecuta el comando **{{prefix}}init**, lo necesito para funcionar correctamente (:" +} \ No newline at end of file diff --git a/src/languages/es-ES/preconditions/roles.json b/src/languages/es-ES/preconditions/roles.json new file mode 100644 index 0000000..3a8667b --- /dev/null +++ b/src/languages/es-ES/preconditions/roles.json @@ -0,0 +1,4 @@ +{ + "rolesActiveOnly_deactivatedRolesError": "{{appName}} no tiene los roles activados. Para hacerlo debes ejecutar `{{prefix}}activateroles`", + "rolesNotActiveOnly_activatedRolesError": "{{appName}} ya tiene los roles inicializados :relieved: revisa los tuyos ejecutando **{{prefix}}myroles**." +} \ No newline at end of file diff --git a/src/languages/es-ES/preconditions/server.json b/src/languages/es-ES/preconditions/server.json new file mode 100644 index 0000000..bd6768f --- /dev/null +++ b/src/languages/es-ES/preconditions/server.json @@ -0,0 +1,4 @@ +{ + "botInitializedOnly_errorMessage": "Primero debes inicializarme ejecutando el comando **{{prefix}}init**.", + "botNotInitializeOnly_errorMessage": "{{appName}} ya ha sido inicializada con anterioridad n.n" +} \ No newline at end of file diff --git a/src/languages/es-ES/preconditions/user.json b/src/languages/es-ES/preconditions/user.json new file mode 100644 index 0000000..f8e0c6b --- /dev/null +++ b/src/languages/es-ES/preconditions/user.json @@ -0,0 +1,3 @@ +{ + "adminOnly_errorMessage": "No tienes permisos para ejecutar este comando. Contacta con un administrador :sweat:" +} \ No newline at end of file diff --git a/src/languages/es-ES/rolesAssignment.json b/src/languages/es-ES/rolesAssignment.json new file mode 100644 index 0000000..737ebbd --- /dev/null +++ b/src/languages/es-ES/rolesAssignment.json @@ -0,0 +1,4 @@ +{ + "userObtainsNewRole": "Felicidades {{username}}, has obtenido el rol '{{roleName}}'!!", + "newRoleAssignmentError": "Fallo en la asignación de rol '{{roleName}}'. Supongo que no tengo suficientes permisos ):" +} \ No newline at end of file diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts index 5c16276..a20240b 100644 --- a/src/listeners/commands/commandError.ts +++ b/src/listeners/commands/commandError.ts @@ -1,23 +1,22 @@ -import { - Listener, - Events, - CommandErrorPayload, - ArgumentError, - UserError, -} from '@sapphire/framework' +import { Listener, Events, ArgumentError, UserError, CommandErrorPayload } from '@sapphire/framework' import { configuration } from '@/config' +import { resolveKey } from '@sapphire/plugin-i18next' +import { languageKeys } from '@/utils' export class UserListener extends Listener { public async run(error: Error, { message, command }: CommandErrorPayload) { let helperMessage: string if (error instanceof ArgumentError) - helperMessage = `That argument is not valid!` + helperMessage = await resolveKey(message, languageKeys.listeners.commands.argumentErrorMessage) else if (error instanceof UserError) - helperMessage = 'You need to write more parameters!' - else helperMessage = 'You need to write more parameters!' + helperMessage = await resolveKey(message, languageKeys.listeners.commands.userErrorMessage) + else helperMessage = await resolveKey(message, languageKeys.listeners.commands.fallbackErrorMessage) - helperMessage += `\n> **Tip**: You can do \`${configuration.prefix}help ${command.name}\` to find out how to use this command.` + helperMessage += await resolveKey(message, languageKeys.listeners.commands.helperMessageExtension, { + prefix: configuration.client.defaultPrefix, + commandName: command.name, + }) return message.channel.send(helperMessage) } } diff --git a/src/listeners/guild/guildCreate.ts b/src/listeners/guild/guildCreate.ts index 32f7fb9..fc3b7ec 100644 --- a/src/listeners/guild/guildCreate.ts +++ b/src/listeners/guild/guildCreate.ts @@ -1,20 +1,21 @@ import { Events, Listener } from '@sapphire/framework' import { Guild, TextChannel } from 'discord.js' import { configuration } from '@/config' +import { resolveKey } from '@sapphire/plugin-i18next' +import { languageKeys } from '../../utils' export class GuildCreateListener extends Listener { - public run(guild: Guild) { + public async run(guild: Guild) { const channel = guild.channels.cache.find( - (channel) => - channel.type === 'GUILD_TEXT' && - channel.permissionsFor(guild.me).has('SEND_MESSAGES') + (channel) => channel.type === 'GUILD_TEXT' && channel.permissionsFor(guild.me).has('SEND_MESSAGES') ) if (channel) { const textChannel = channel as TextChannel - textChannel.send( - `Thanks for invite me to your server n.n please, first run the **${configuration.prefix}init** command, I need it to work correctly (:` - ) + const welcomeMessage = await resolveKey(guild, languageKeys.listeners.guild.welcomeMessage, { + prefix: configuration.client.defaultPrefix, + }) + textChannel.send(welcomeMessage) } } } diff --git a/src/listeners/guild/guildDelete.ts b/src/listeners/guild/guildDelete.ts index 9e58595..e266d0f 100644 --- a/src/listeners/guild/guildDelete.ts +++ b/src/listeners/guild/guildDelete.ts @@ -6,12 +6,9 @@ export class GuildDeleteListener extends Listener { public async run(guild: Guild) { const { id: guildId } = guild try { - logger.info( - `Trying to leave '${guild.name}' server and delete from DB...`, - { - context: guild.client.constructor.name, - } - ) + logger.info(`Trying to leave '${guild.name}' server and delete from DB...`, { + context: guild.client.constructor.name, + }) await container.db.guildService.deleteById(guildId) await container.db.userService.deleteGuildDataById(guildId) if (guild.me.permissions.has('MANAGE_ROLES')) { @@ -21,12 +18,9 @@ export class GuildDeleteListener extends Listener { context: container.client.constructor.name, }) } catch (error) { - logger.error( - `MongoDB Connection error. Could not delete '${guild.name}' server from DB`, - { - context: container.client.constructor.name, - } - ) + logger.error(`MongoDB Connection error. Could not delete '${guild.name}' server from DB`, { + context: container.client.constructor.name, + }) } } } diff --git a/src/listeners/messages/messageCreate.ts b/src/listeners/messages/messageCreate.ts index a67b6e6..a82af6c 100644 --- a/src/listeners/messages/messageCreate.ts +++ b/src/listeners/messages/messageCreate.ts @@ -5,16 +5,14 @@ import { GuildData, User as DbUser } from '@/database' import { defineRoles, logger } from '@/utils' import { DocumentType } from '@typegoose/typegoose' -export class MessageCreateListener extends Listener< - typeof Events.MessageCreate -> { +export class MessageCreateListener extends Listener { public async run(message: Message) { const notAllowedPrefix = ['>', '#', '$', '!', ';', 'rpg'] const { content, author, guild } = message if ( author.bot || - content.startsWith(configuration.prefix) || + content.startsWith(configuration.client.defaultPrefix) || guild === null || notAllowedPrefix.some((prefix) => content.startsWith(prefix)) ) { @@ -42,9 +40,7 @@ export class MessageCreateListener extends Listener< // Give points to valid messages. let score = 0 - const validWords = messageWords.filter( - (word) => word.length >= 2 && !word.match(userRegex) - ).length + const validWords = messageWords.filter((word) => word.length >= 2 && !word.match(userRegex)).length if (validWords >= 3) { score += 3 } @@ -59,29 +55,21 @@ export class MessageCreateListener extends Listener< score += 2 } } catch (error) { - logger.error( - 'There was a problem registering score from user interaction', - { - context: container.client.constructor.name, - } - ) + logger.error('There was a problem registering score from user interaction', { + context: container.client.constructor.name, + }) } } }) try { - const user: DocumentType = await container.db.userService.getById( - author.id - ) + const user: DocumentType = await container.db.userService.getById(author.id) let finalParticipationScore = score if (user) { - const guildDataIdx = user.guildsData.findIndex( - (guildData) => guildData.guildId === guildId - ) + const guildDataIdx = user.guildsData.findIndex((guildData) => guildData.guildId === guildId) user.guildsData[guildDataIdx].participationScore += score - finalParticipationScore = - user.guildsData[guildDataIdx].participationScore + finalParticipationScore = user.guildsData[guildDataIdx].participationScore await user.save() } else { const guildData: GuildData = { @@ -99,12 +87,9 @@ export class MessageCreateListener extends Listener< const authorGuildMember = await message.guild.members.fetch(author.id) defineRoles(finalParticipationScore, authorGuildMember, message) } catch (error) { - logger.error( - `MongoDB Connection error. Could not register ${author.username}'s words points`, - { - context: container.client.constructor.name, - } - ) + logger.error(`MongoDB Connection error. Could not register ${author.username}'s words points`, { + context: container.client.constructor.name, + }) } } } diff --git a/src/listeners/system/ready.ts b/src/listeners/system/ready.ts index b2407eb..f9f2207 100644 --- a/src/listeners/system/ready.ts +++ b/src/listeners/system/ready.ts @@ -12,8 +12,8 @@ export class ReadyListener extends Listener { } public run(client: Client) { - client.user.setActivity(`${configuration.prefix}help`) - logger.info(`${client.user.username} is online n.n`, { + client.user.setActivity(`${configuration.client.defaultPrefix}help`) + logger.info(`${client.user.username} is online.`, { context: client.constructor.name, }) } diff --git a/src/preconditions/roles/RolesActiveOnly.ts b/src/preconditions/roles/RolesActivatedOnly.ts similarity index 54% rename from src/preconditions/roles/RolesActiveOnly.ts rename to src/preconditions/roles/RolesActivatedOnly.ts index 471b8d9..a53dd0e 100644 --- a/src/preconditions/roles/RolesActiveOnly.ts +++ b/src/preconditions/roles/RolesActivatedOnly.ts @@ -1,11 +1,21 @@ import { container, Precondition } from '@sapphire/framework' import { Message } from 'discord.js' import { configuration } from '@/config' +import { resolveKey } from '@sapphire/plugin-i18next' +import { languageKeys } from '@/utils' -export class RolesActiveOnlyPrecondition extends Precondition { +export class RolesActivatedOnlyPrecondition extends Precondition { public async run(message: Message) { const { id: guildId } = message.guild - const deactiveRolesError = `${configuration.appName}'s roles are not activated. First, you have to run \`${configuration.prefix}activateroles\`` + const { + appName, + client: { defaultPrefix: prefix }, + } = configuration + const deactiveRolesError = await resolveKey( + message, + languageKeys.preconditions.roles.rolesActiveOnlyDeactivatedRolesError, + { appName, prefix } + ) const cachedGuild = container.cache.get(guildId) if (cachedGuild && !cachedGuild.rolesActivated) { return this.error({ message: deactiveRolesError }) @@ -17,8 +27,9 @@ export class RolesActiveOnlyPrecondition extends Precondition { return this.error({ message: deactiveRolesError }) } } catch (error) { + const unexpectedErrorMessage = await resolveKey(message, languageKeys.errors.unexpectedError) return this.error({ - message: 'It occured an unexpected error :sweat: try again later.', + message: unexpectedErrorMessage, }) } diff --git a/src/preconditions/roles/RolesNotActiveOnly.ts b/src/preconditions/roles/RolesDeactivatedOnly.ts similarity index 53% rename from src/preconditions/roles/RolesNotActiveOnly.ts rename to src/preconditions/roles/RolesDeactivatedOnly.ts index 17ccd23..b936705 100644 --- a/src/preconditions/roles/RolesNotActiveOnly.ts +++ b/src/preconditions/roles/RolesDeactivatedOnly.ts @@ -1,11 +1,21 @@ import { container, Precondition } from '@sapphire/framework' import { Message } from 'discord.js' import { configuration } from '@/config' +import { resolveKey } from '@sapphire/plugin-i18next' +import { languageKeys } from '@/utils' -export class RolesNotActiveOnlyPrecondition extends Precondition { +export class RolesDeactivatedOnlyPrecondition extends Precondition { public async run(message: Message) { const { id: guildId } = message.guild - const activatedRolesError = `You already have initialize ${configuration.appName}'s roles :relieved: Check yours with **${configuration.prefix}myroles**.` + const { + appName, + client: { defaultPrefix: prefix }, + } = configuration + const activatedRolesError = await resolveKey( + message, + languageKeys.preconditions.roles.rolesNotActiveOnlyActivatedRolesError, + { appName, prefix } + ) const cachedGuild = container.cache.get(guildId) if (cachedGuild?.rolesActivated) { return this.error({ message: activatedRolesError }) @@ -17,8 +27,9 @@ export class RolesNotActiveOnlyPrecondition extends Precondition { return this.error({ message: activatedRolesError }) } } catch (error) { + const unexpectedErrorMessage = await resolveKey(message, languageKeys.errors.unexpectedError) return this.error({ - message: 'It occured an unexpected error :sweat: try again later.', + message: unexpectedErrorMessage, }) } diff --git a/src/preconditions/server/BotInitializeOnly.ts b/src/preconditions/server/BotInitializedOnly.ts similarity index 51% rename from src/preconditions/server/BotInitializeOnly.ts rename to src/preconditions/server/BotInitializedOnly.ts index 133afda..5e1e89d 100644 --- a/src/preconditions/server/BotInitializeOnly.ts +++ b/src/preconditions/server/BotInitializedOnly.ts @@ -1,8 +1,10 @@ import { container, Precondition } from '@sapphire/framework' import { Message } from 'discord.js' import { configuration } from '@/config' +import { resolveKey } from '@sapphire/plugin-i18next' +import { languageKeys } from '@/utils' -export class BotInitializeOnlyPrecondition extends Precondition { +export class BotInitializedOnlyPrecondition extends Precondition { public async run(message: Message) { const { id: guildId } = message.guild const guild = await container.db.guildService.getById(guildId) @@ -10,7 +12,9 @@ export class BotInitializeOnlyPrecondition extends Precondition { return guild ? this.ok() : this.error({ - message: `First you have to initialize me running **${configuration.prefix}init** command.`, + message: await resolveKey(message, languageKeys.preconditions.server.botInitializedOnlyErrorMessage, { + prefix: configuration.client.defaultPrefix, + }), }) } } diff --git a/src/preconditions/server/BotNotInitializeOnly.ts b/src/preconditions/server/BotNotInitializedOnly.ts similarity index 51% rename from src/preconditions/server/BotNotInitializeOnly.ts rename to src/preconditions/server/BotNotInitializedOnly.ts index a7ff171..1ee8d87 100644 --- a/src/preconditions/server/BotNotInitializeOnly.ts +++ b/src/preconditions/server/BotNotInitializedOnly.ts @@ -1,8 +1,10 @@ import { container, Precondition } from '@sapphire/framework' import { Message } from 'discord.js' import { configuration } from '@/config' +import { resolveKey } from '@sapphire/plugin-i18next' +import { languageKeys } from '@/utils' -export class BotNotInitializeOnlyPrecondition extends Precondition { +export class BotNotInitializedOnlyPrecondition extends Precondition { public async run(message: Message) { const { id: guildId } = message.guild const guild = await container.db.guildService.getById(guildId) @@ -10,7 +12,9 @@ export class BotNotInitializeOnlyPrecondition extends Precondition { return !guild ? this.ok() : this.error({ - message: `${configuration.appName} has already been initialize n.n`, + message: await resolveKey(message, languageKeys.preconditions.server.botNotInitializedOnlyErrorMessage, { + appName: configuration.appName, + }), }) } } diff --git a/src/preconditions/user/AdminOnly.ts b/src/preconditions/user/AdminOnly.ts index d8563ae..f456a8a 100644 --- a/src/preconditions/user/AdminOnly.ts +++ b/src/preconditions/user/AdminOnly.ts @@ -1,16 +1,16 @@ import { Precondition } from '@sapphire/framework' +import { resolveKey } from '@sapphire/plugin-i18next' import { GuildMember, Message } from 'discord.js' +import { languageKeys } from '@/utils' export class AdminOnlyPrecondition extends Precondition { public async run(message: Message) { - const user: GuildMember = await message.guild.members.fetch( - message.author.id - ) + const user: GuildMember = await message.guild.members.fetch(message.author.id) return user.permissions.has('ADMINISTRATOR') ? this.ok() : this.error({ - message: `You don't have permissions to run this command. Contact with an Administrator :sweat:`, + message: await resolveKey(message, languageKeys.preconditions.user.adminOnlyErrorMessage), }) } } diff --git a/src/utils/command.ts b/src/utils/command.ts new file mode 100644 index 0000000..985ece5 --- /dev/null +++ b/src/utils/command.ts @@ -0,0 +1,31 @@ +import { Args, Command, CommandContext } from '@sapphire/framework' +import { fetchT, TFunction } from '@sapphire/plugin-i18next' +import { Message } from 'discord.js' +import * as Lexure from 'lexure' + +export abstract class CustomCommand extends Command { + constructor(context: Command.Context, options: Command.Options) { + super(context, { ...options }) + } + + /** + * The pre-parse method. This method can be overridden by plugins to define their own argument parser. + * @param message The message that triggered the command. + * @param parameters The raw parameters as a single string. + * @param context The command-context used in this execution. + */ + public async preParse(message: Message, parameters: string, context: CommandContext): Promise { + const parser = new Lexure.Parser(this.lexer.setInput(parameters).lex()).setUnorderedStrategy(this.strategy) + const args = new Lexure.Args(parser.parse()) + return new CustomArgs(message, this, args, context, await fetchT(message)) + } +} + +export class CustomArgs extends Args { + public t: TFunction // result of 'await fetchT', obtains locale texts. + + public constructor(message: Message, command: Command, parser: Lexure.Args, context: CommandContext, t: TFunction) { + super(message, command, parser, context) + this.t = t + } +} diff --git a/src/utils/functions/buttons.ts b/src/utils/functions/buttons.ts index 4445f89..1187c09 100644 --- a/src/utils/functions/buttons.ts +++ b/src/utils/functions/buttons.ts @@ -1,18 +1,5 @@ -import { - MessageButtonStyle, - EmojiIdentifierResolvable, - MessageButton, -} from 'discord.js' +import { MessageButtonStyle, EmojiIdentifierResolvable, MessageButton } from 'discord.js' -export const getButton = ( - id: string, - label: string, - style: MessageButtonStyle, - emoji?: EmojiIdentifierResolvable -) => { - return new MessageButton() - .setCustomId(id) - .setEmoji(emoji) - .setLabel(label) - .setStyle(style) +export const getButton = (id: string, label: string, style: MessageButtonStyle, emoji?: EmojiIdentifierResolvable) => { + return new MessageButton().setCustomId(id).setEmoji(emoji).setLabel(label).setStyle(style) } diff --git a/src/utils/functions/files/index.ts b/src/utils/functions/files/index.ts index e61d379..96eede8 100644 --- a/src/utils/functions/files/index.ts +++ b/src/utils/functions/files/index.ts @@ -1,10 +1,7 @@ import { MessageAttachment } from 'discord.js' export const getBotLogo = () => { - return new MessageAttachment( - './public/img/chibiKnightLogo.png', - 'chibiKnightLogo.png' - ) + return new MessageAttachment('./public/img/chibiKnightLogo.png', 'chibiKnightLogo.png') } export const botLogoURL = 'attachment://chibiKnightLogo.png' diff --git a/src/utils/functions/games/index.ts b/src/utils/functions/games/index.ts index 70cc275..4052fe4 100644 --- a/src/utils/functions/games/index.ts +++ b/src/utils/functions/games/index.ts @@ -1,6 +1,8 @@ +import { TFunction } from '@sapphire/plugin-i18next' import { getButton } from '..' +import { languageKeys } from '../..' export * from './tictactoe' -export const getCancelGameButton = (label: string) => - getButton(label, 'CANCEL GAME', 'DANGER') +export const getCancelGameButton = (label: string, t: TFunction) => + getButton(label, t(languageKeys.commands.games.gameCancelledButtonLabel), 'DANGER') diff --git a/src/utils/functions/games/tictactoe.ts b/src/utils/functions/games/tictactoe.ts index 7fee858..cecc0ed 100644 --- a/src/utils/functions/games/tictactoe.ts +++ b/src/utils/functions/games/tictactoe.ts @@ -1,4 +1,3 @@ import { getButton } from '..' -export const getTttMoveButton = (idx: number) => - getButton(`ttt-move-${idx}`, idx.toString(), 'PRIMARY') +export const getTttMoveButton = (idx: number) => getButton(`ttt-move-${idx}`, idx.toString(), 'PRIMARY') diff --git a/src/utils/i18n/index.ts b/src/utils/i18n/index.ts new file mode 100644 index 0000000..bc0ceb3 --- /dev/null +++ b/src/utils/i18n/index.ts @@ -0,0 +1,26 @@ +import { container } from '@sapphire/framework' +import { Guild } from '@/database' +import { InternationalizationContext } from '@sapphire/plugin-i18next' +import { LocaleCodes } from './locales' + +export * as languageKeys from './keys' +export * from './locales' + +export const i18nConfig = { + fetchLanguage: async ({ guild }: InternationalizationContext) => { + if (!guild) return LocaleCodes.DEFAULT + + const cachedGuild: Guild = container.cache.get(guild.id) + if (cachedGuild?.guildLanguage) { + return cachedGuild.guildLanguage + } + + const dbGuild = await container.db.guildService.getById(guild.id) + if (dbGuild?.guildLanguage) { + return dbGuild.guildLanguage + } + + return LocaleCodes.DEFAULT + }, + defaultName: LocaleCodes.DEFAULT, +} diff --git a/src/utils/i18n/keys/categories.ts b/src/utils/i18n/keys/categories.ts new file mode 100644 index 0000000..af34af6 --- /dev/null +++ b/src/utils/i18n/keys/categories.ts @@ -0,0 +1,5 @@ +const BASE_PATH = 'categories' + +export const gamesCategoryDecoratedTitle = `${BASE_PATH}:gamesCategoryDecoratedTitle` +export const miscCategoryDecoratedTitle = `${BASE_PATH}:miscCategoryDecoratedTitle` +export const rolesCategoryDecoratedTitle = `${BASE_PATH}:rolesCategoryDecoratedTitle` diff --git a/src/utils/i18n/keys/commands/games/basePath.ts b/src/utils/i18n/keys/commands/games/basePath.ts new file mode 100644 index 0000000..f102dc6 --- /dev/null +++ b/src/utils/i18n/keys/commands/games/basePath.ts @@ -0,0 +1 @@ +export const BASE_PATH = 'commands/games' diff --git a/src/utils/i18n/keys/commands/games/cancelgame.ts b/src/utils/i18n/keys/commands/games/cancelgame.ts new file mode 100644 index 0000000..beb3163 --- /dev/null +++ b/src/utils/i18n/keys/commands/games/cancelgame.ts @@ -0,0 +1,5 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:cancelgameDescription` +export const noActiveGame = `${BASE_PATH}:cancelgameNoActiveGame` +export const gameCancelled = `${BASE_PATH}:cancelgameGameCancelled` diff --git a/src/utils/i18n/keys/commands/games/index.ts b/src/utils/i18n/keys/commands/games/index.ts new file mode 100644 index 0000000..b262fe8 --- /dev/null +++ b/src/utils/i18n/keys/commands/games/index.ts @@ -0,0 +1,7 @@ +import { BASE_PATH } from './basePath' + +export * as cancelgame from './cancelgame' +export * as tictactoe from './tictactoe' +export * as tictactoeleaderboard from './tictactoeleaderboard' + +export const gameCancelledButtonLabel = `${BASE_PATH}:cancelGameButtonLabel` diff --git a/src/utils/i18n/keys/commands/games/tictactoe.ts b/src/utils/i18n/keys/commands/games/tictactoe.ts new file mode 100644 index 0000000..0e97d34 --- /dev/null +++ b/src/utils/i18n/keys/commands/games/tictactoe.ts @@ -0,0 +1,29 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:tictactoeDescription` +export const activeGameInstance = `${BASE_PATH}:tictactoeActiveGameInstance` +export const challengeYourself = `${BASE_PATH}:tictactoeChallengeYourself` +export const challengeBot = `${BASE_PATH}:tictactoeChallengeBot` +export const acceptChallengeButtonLabel = `${BASE_PATH}:tictactoeAcceptChallengeButtonLabel` +export const rejectChallengeButtonLabel = `${BASE_PATH}:tictactoeRejectChallengeButtonLabel` +export const startGameQuestion = `${BASE_PATH}:tictactoeStartGameQuestion` +export const rejectChallengeMessage = `${BASE_PATH}:tictactoeRejectChallengeMessage` +export const ignoreChallengeMessage = `${BASE_PATH}:tictactoeIgnoreChallengeMessage` +export const errorMessage = `${BASE_PATH}:tictactoeErrorMessage` +export const instructionsTitle = `${BASE_PATH}:tictactoeInstructionsTitle` +export const instructionsText = `${BASE_PATH}:tictactoeInstructionsText` +export const currentTurnTitle = `${BASE_PATH}:tictactoeCurrentTurnLabel` +export const currentTurnText = `${BASE_PATH}:tictactoeCurrentTurnText` +export const resultText = `${BASE_PATH}:tictactoeResultText` +export const consolationPrizeTitleOneLoser = `${BASE_PATH}:tictactoeConsolationPrizeTitleOneLoser` +export const consolationPrizeTextOneLoser = `${BASE_PATH}:tictactoeConsolationPrizeTextOneLoser` +export const consolationPrizeTitleTwoLosers = `${BASE_PATH}:tictactoeConsolationPrizeTitleTwoLosers` +export const consolationPrizeTextTwoLosers = `${BASE_PATH}:tictactoeConsolationPrizeTextTwoLosers` +export const gameTitle = `${BASE_PATH}:tictactoeGameTitle` +export const gameDescription = `${BASE_PATH}:tictactoeGameDescription` +export const challengersTitle = `${BASE_PATH}:tictactoeChallengersTitle` +export const challengersText = `${BASE_PATH}:tictactoeChallengersText` +export const boardTitle = `${BASE_PATH}:tictactoeBoardTitle` +export const positionsReferenceTitle = `${BASE_PATH}:tictactoePositionsReferenceTitle` +export const victoryResult = `${BASE_PATH}:tictactoeVictoryResult` +export const tieResult = `${BASE_PATH}:tictactoeTieResult` diff --git a/src/utils/i18n/keys/commands/games/tictactoeleaderboard.ts b/src/utils/i18n/keys/commands/games/tictactoeleaderboard.ts new file mode 100644 index 0000000..c12c112 --- /dev/null +++ b/src/utils/i18n/keys/commands/games/tictactoeleaderboard.ts @@ -0,0 +1,8 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:tictactoeleaderboardDescription` +export const messageTitle = `${BASE_PATH}:tictactoeleaderboardMessageTitle` +export const messageDescription = `${BASE_PATH}:tictactoeleaderboardMessageDescription` +export const positioningLabel = `${BASE_PATH}:tictactoeleaderboardPositioningLabel` +export const victoriesLabel = `${BASE_PATH}:tictactoeleaderboardVictoriesLabel` +export const errorMessage = `${BASE_PATH}:tictactoeleaderboardErrorMessage` diff --git a/src/utils/i18n/keys/commands/index.ts b/src/utils/i18n/keys/commands/index.ts new file mode 100644 index 0000000..970c773 --- /dev/null +++ b/src/utils/i18n/keys/commands/index.ts @@ -0,0 +1,3 @@ +export * as games from './games' +export * as misc from './misc' +export * as roles from './roles' diff --git a/src/utils/i18n/keys/commands/misc/basePath.ts b/src/utils/i18n/keys/commands/misc/basePath.ts new file mode 100644 index 0000000..543457d --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/basePath.ts @@ -0,0 +1 @@ +export const BASE_PATH = 'commands/misc' diff --git a/src/utils/i18n/keys/commands/misc/congratulate.ts b/src/utils/i18n/keys/commands/misc/congratulate.ts new file mode 100644 index 0000000..5aa939c --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/congratulate.ts @@ -0,0 +1,4 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:congratulateDescription` +export const embedMessageDescription = `${BASE_PATH}:congratulateEmbedMessageDescription` diff --git a/src/utils/i18n/keys/commands/misc/help.ts b/src/utils/i18n/keys/commands/misc/help.ts new file mode 100644 index 0000000..bab79fa --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/help.ts @@ -0,0 +1,7 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:helpDescription` +export const unknownCommandError = `${BASE_PATH}:helpUnknownCommandError` +export const everyCommandEmbedMessageDescription = `${BASE_PATH}:helpEveryCommandEmbedMessageDescription` +export const everyCommandEmbedMessageFooter = `${BASE_PATH}:helpEveryCommandEmbedMessageFooter` +export const commandWithoutDescription = `${BASE_PATH}:helpCommandWithoutDescription` diff --git a/src/utils/i18n/keys/commands/misc/index.ts b/src/utils/i18n/keys/commands/misc/index.ts new file mode 100644 index 0000000..b158d30 --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/index.ts @@ -0,0 +1,6 @@ +export * as congratulate from './congratulate' +export * as help from './help' +export * as init from './init' +export * as say from './say' +export * as selectlanguage from './selectlanguage' +export * as shameonyou from './shameonyou' diff --git a/src/utils/i18n/keys/commands/misc/init.ts b/src/utils/i18n/keys/commands/misc/init.ts new file mode 100644 index 0000000..a591968 --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/init.ts @@ -0,0 +1,5 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:initDescription` +export const initSuccessfullyEvent = `${BASE_PATH}:initSuccessfullyEvent` +export const initFailureEvent = `${BASE_PATH}:initFailureEvent` diff --git a/src/utils/i18n/keys/commands/misc/say.ts b/src/utils/i18n/keys/commands/misc/say.ts new file mode 100644 index 0000000..ccfb665 --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/say.ts @@ -0,0 +1,3 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:sayDescription` diff --git a/src/utils/i18n/keys/commands/misc/selectlanguage.ts b/src/utils/i18n/keys/commands/misc/selectlanguage.ts new file mode 100644 index 0000000..77b3209 --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/selectlanguage.ts @@ -0,0 +1,6 @@ +import { BASE_PATH } from './basePath' + +export const commandDescription = `${BASE_PATH}:selectLanguageCommandDescription` +export const messageContent = `${BASE_PATH}:selectLanguageMessageContent` +export const ignoreMessage = `${BASE_PATH}:selectLanguageIgnoreMessage` +export const languageChangedMessage = `${BASE_PATH}:selectLanguage_LanguageChangedMessage` diff --git a/src/utils/i18n/keys/commands/misc/shameonyou.ts b/src/utils/i18n/keys/commands/misc/shameonyou.ts new file mode 100644 index 0000000..44662ed --- /dev/null +++ b/src/utils/i18n/keys/commands/misc/shameonyou.ts @@ -0,0 +1,4 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:shameonyouDescription` +export const embedMessageDescription = `${BASE_PATH}:shameonyouEmbedMessageDescription` diff --git a/src/utils/i18n/keys/commands/roles/activateroles.ts b/src/utils/i18n/keys/commands/roles/activateroles.ts new file mode 100644 index 0000000..9c45e75 --- /dev/null +++ b/src/utils/i18n/keys/commands/roles/activateroles.ts @@ -0,0 +1,13 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:activaterolesCommandDescription` +export const rolesListText = `${BASE_PATH}:activaterolesRolesListText` +export const messageFooter = `${BASE_PATH}:activaterolesMessageFooter` +export const acceptButtonLabel = `${BASE_PATH}:activaterolesAcceptLabel` +export const rejectButtonLabel = `${BASE_PATH}:activaterolesRejectLabel` +export const unexpectedError = `${BASE_PATH}:activaterolesUnexpectedError` +export const activateMessage = `${BASE_PATH}:activaterolesActivateText` +export const rolesCreationError = `${BASE_PATH}:activaterolesRolesCreationError` +export const rolesCreationSuccessful = `${BASE_PATH}:activaterolesRolesCreationSuccessful` +export const rejectMessage = `${BASE_PATH}:activaterolesRejectText` +export const ignoreMessage = `${BASE_PATH}:activaterolesIgnoreText` diff --git a/src/utils/i18n/keys/commands/roles/basePath.ts b/src/utils/i18n/keys/commands/roles/basePath.ts new file mode 100644 index 0000000..2c4a7b1 --- /dev/null +++ b/src/utils/i18n/keys/commands/roles/basePath.ts @@ -0,0 +1 @@ +export const BASE_PATH = 'commands/roles' diff --git a/src/utils/i18n/keys/commands/roles/index.ts b/src/utils/i18n/keys/commands/roles/index.ts new file mode 100644 index 0000000..f6ed00c --- /dev/null +++ b/src/utils/i18n/keys/commands/roles/index.ts @@ -0,0 +1,3 @@ +export * as activateroles from './activateroles' +export * as myrole from './myrole' +export * as roles from './roles' diff --git a/src/utils/i18n/keys/commands/roles/myrole.ts b/src/utils/i18n/keys/commands/roles/myrole.ts new file mode 100644 index 0000000..e27c612 --- /dev/null +++ b/src/utils/i18n/keys/commands/roles/myrole.ts @@ -0,0 +1,12 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:myroleCommandDescription` +export const embedMessageDescription = `${BASE_PATH}:myroleEmbedMessageDescription` +export const followingRoleTitle = `${BASE_PATH}:myroleFollowingRoleTitle` +export const noHaveRoleTitle = `${BASE_PATH}:myroleHaveNoRoleTitle` +export const noHaveRoleText = `${BASE_PATH}:myroleHaveNoRoleText` +export const currentScoreTitle = `${BASE_PATH}:myroleCurrentScoreTitle` +export const currentScoreText = `${BASE_PATH}:myroleCurrentScoreText` +export const undefinedScoreText = `${BASE_PATH}:myroleUndefinedScoreText` +export const messageFooter = `${BASE_PATH}:myroleMessageFooter` +export const messageFooterMaxPoints = `${BASE_PATH}:myroleMessageFooterMaxPoints` diff --git a/src/utils/i18n/keys/commands/roles/roles.ts b/src/utils/i18n/keys/commands/roles/roles.ts new file mode 100644 index 0000000..ca8e5db --- /dev/null +++ b/src/utils/i18n/keys/commands/roles/roles.ts @@ -0,0 +1,13 @@ +import { BASE_PATH } from './basePath' + +export const description = `${BASE_PATH}:rolesCommandDescription` +export const currentRoleTitle = `${BASE_PATH}:rolesCurrentRoleTitle` +export const noCurrentRoleText = `${BASE_PATH}:rolesNoCurrentRoleText` +export const userRolesMessageDescription = `${BASE_PATH}:rolesUserRolesMessageDescription` +export const currentScoreTitle = `${BASE_PATH}:rolesCurrentScoreTitle` +export const currentScoreText = `${BASE_PATH}:rolesCurrentScoreText` +export const undefinedScoreText = `${BASE_PATH}:rolesCurrentScoreText` +export const everyRoleMessageDescription = `${BASE_PATH}:rolesEveryRoleMessageDescription` +export const rolesTitle = `${BASE_PATH}:rolesRolesTitle` +export const requiredScoresTitle = `${BASE_PATH}:rolesRequiredScoresTitle` +export const messageFooter = `${BASE_PATH}:rolesMessageFooter` diff --git a/src/utils/i18n/keys/errors.ts b/src/utils/i18n/keys/errors.ts new file mode 100644 index 0000000..9c3d56e --- /dev/null +++ b/src/utils/i18n/keys/errors.ts @@ -0,0 +1,3 @@ +const BASE_PATH = 'errors' + +export const unexpectedError = `${BASE_PATH}:unexpectedError` diff --git a/src/utils/i18n/keys/index.ts b/src/utils/i18n/keys/index.ts new file mode 100644 index 0000000..b8c87b9 --- /dev/null +++ b/src/utils/i18n/keys/index.ts @@ -0,0 +1,6 @@ +export * as commands from './commands' +export * as listeners from './listeners' +export * as preconditions from './preconditions' +export * as categories from './categories' +export * as errors from './errors' +export * as rolesAssignment from './rolesAssignment' diff --git a/src/utils/i18n/keys/listeners/commands.ts b/src/utils/i18n/keys/listeners/commands.ts new file mode 100644 index 0000000..bc6d71f --- /dev/null +++ b/src/utils/i18n/keys/listeners/commands.ts @@ -0,0 +1,6 @@ +const BASE_PATH = 'listeners/commands' + +export const argumentErrorMessage = `${BASE_PATH}:argumentErrorMessage` +export const userErrorMessage = `${BASE_PATH}:userErrorMessage` +export const fallbackErrorMessage = `${BASE_PATH}:fallbackErrorMessage` +export const helperMessageExtension = `${BASE_PATH}:helperMessageExtension` diff --git a/src/utils/i18n/keys/listeners/guild.ts b/src/utils/i18n/keys/listeners/guild.ts new file mode 100644 index 0000000..10567aa --- /dev/null +++ b/src/utils/i18n/keys/listeners/guild.ts @@ -0,0 +1,3 @@ +const BASE_PATH = 'listeners/commands' + +export const welcomeMessage = `${BASE_PATH}:guildCreateWelcomeMessage` diff --git a/src/utils/i18n/keys/listeners/index.ts b/src/utils/i18n/keys/listeners/index.ts new file mode 100644 index 0000000..dcd925e --- /dev/null +++ b/src/utils/i18n/keys/listeners/index.ts @@ -0,0 +1,2 @@ +export * as commands from './commands' +export * as guild from './guild' diff --git a/src/utils/i18n/keys/preconditions/index.ts b/src/utils/i18n/keys/preconditions/index.ts new file mode 100644 index 0000000..87ff030 --- /dev/null +++ b/src/utils/i18n/keys/preconditions/index.ts @@ -0,0 +1,3 @@ +export * as roles from './roles' +export * as server from './server' +export * as user from './user' diff --git a/src/utils/i18n/keys/preconditions/roles.ts b/src/utils/i18n/keys/preconditions/roles.ts new file mode 100644 index 0000000..549a0f8 --- /dev/null +++ b/src/utils/i18n/keys/preconditions/roles.ts @@ -0,0 +1,4 @@ +const BASE_PATH = 'preconditions/roles' + +export const rolesActiveOnlyDeactivatedRolesError = `${BASE_PATH}:rolesActiveOnly_deactivatedRolesError` +export const rolesNotActiveOnlyActivatedRolesError = `${BASE_PATH}:rolesNotActiveOnly_activatedRolesError` diff --git a/src/utils/i18n/keys/preconditions/server.ts b/src/utils/i18n/keys/preconditions/server.ts new file mode 100644 index 0000000..29b2cd1 --- /dev/null +++ b/src/utils/i18n/keys/preconditions/server.ts @@ -0,0 +1,4 @@ +const BASE_PATH = 'preconditions/server' + +export const botInitializedOnlyErrorMessage = `${BASE_PATH}:botInitializedOnly_errorMessage` +export const botNotInitializedOnlyErrorMessage = `${BASE_PATH}:botNotInitializeOnly_errorMessage` diff --git a/src/utils/i18n/keys/preconditions/user.ts b/src/utils/i18n/keys/preconditions/user.ts new file mode 100644 index 0000000..3dc1733 --- /dev/null +++ b/src/utils/i18n/keys/preconditions/user.ts @@ -0,0 +1,3 @@ +const BASE_PATH = 'preconditions/user' + +export const adminOnlyErrorMessage = `${BASE_PATH}:adminOnly_errorMessage` diff --git a/src/utils/i18n/keys/rolesAssignment.ts b/src/utils/i18n/keys/rolesAssignment.ts new file mode 100644 index 0000000..d768c87 --- /dev/null +++ b/src/utils/i18n/keys/rolesAssignment.ts @@ -0,0 +1,4 @@ +const BASE_PATH = 'roles-assignment' + +export const userObtainsNewRole = `${BASE_PATH}:userObtainsNewRole` +export const newRoleAssignmentError = `${BASE_PATH}:newRoleAssignmentError` diff --git a/src/utils/i18n/locales.ts b/src/utils/i18n/locales.ts new file mode 100644 index 0000000..cd12bcf --- /dev/null +++ b/src/utils/i18n/locales.ts @@ -0,0 +1,22 @@ +export enum LocaleCodes { + DEFAULT = 'en-US', + SPANISH_ES = 'es-ES', +} + +export enum Languages { + ESPANOL = 'Español', + ENGLISH = 'English', +} + +export type LocaleCodesProperty = 'emoji' | 'language' + +export const languagesTypes: Record> = { + [LocaleCodes.DEFAULT]: { + emoji: '🇺🇸', + language: Languages.ENGLISH, + }, + [LocaleCodes.SPANISH_ES]: { + emoji: '🇪🇸', + language: Languages.ESPANOL, + }, +} diff --git a/src/utils/index.ts b/src/utils/index.ts index c31e620..d35a066 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,6 +1,8 @@ -export * from './types' +export * from './types' // This has to be in first place export * from './functions' +export * from './i18n' export * from './links' export * from './objects' +export * from './command' export * from './logger' export * from './role.util' diff --git a/src/utils/links/commands.ts b/src/utils/links/commands.ts index 5d873e0..ead71bf 100644 --- a/src/utils/links/commands.ts +++ b/src/utils/links/commands.ts @@ -8,9 +8,7 @@ export interface CommandsLinks { export const commandsLinks: CommandsLinks = { [BotCommandsCategories.GAMES]: { tictactoe: { - gifs: [ - 'https://media.tenor.com/images/5d12401ee6fa62de116e70d3c99bb4cc/tenor.gif', - ], + gifs: ['https://media.tenor.com/images/5d12401ee6fa62de116e70d3c99bb4cc/tenor.gif'], }, cancelGame: {}, tictactoeleaderboard: {}, diff --git a/src/utils/links/utils.ts b/src/utils/links/utils.ts index f5d3d3b..c8bed70 100644 --- a/src/utils/links/utils.ts +++ b/src/utils/links/utils.ts @@ -2,9 +2,7 @@ import { UtilsLinks } from '../types' export const utilLinks: UtilsLinks = { roles: { - upgradeRole: - 'https://media1.tenor.com/images/e3da22e65b21255ae292ad5fa051e030/tenor.gif', - noRole: - 'https://media1.tenor.com/images/ba7c371ec27c24b0b2725e34daf4afde/tenor.gif', + upgradeRole: 'https://media1.tenor.com/images/e3da22e65b21255ae292ad5fa051e030/tenor.gif', + noRole: 'https://media1.tenor.com/images/ba7c371ec27c24b0b2725e34daf4afde/tenor.gif', }, } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2899a54..a6ca75b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -6,19 +6,11 @@ interface LoggerOptions { export const logger = { debug: (values: string, options?: LoggerOptions) => - container.logger.debug( - `(${options.context}): ${values}` as unknown as unknown[] - ), + container.logger.debug(`(${options.context}): ${values}` as unknown as unknown[]), error: (values: string, options?: LoggerOptions) => - container.logger.error( - `(${options.context}): ${values}` as unknown as unknown[] - ), + container.logger.error(`(${options.context}): ${values}` as unknown as unknown[]), warn: (values: string, options?: LoggerOptions) => - container.logger.warn( - `(${options.context}): ${values}` as unknown as unknown[] - ), + container.logger.warn(`(${options.context}): ${values}` as unknown as unknown[]), info: (values: string, options?: LoggerOptions) => - container.logger.info( - `(${options.context}): ${values}` as unknown as unknown[] - ), + container.logger.info(`(${options.context}): ${values}` as unknown as unknown[]), } diff --git a/src/utils/objects/commands.ts b/src/utils/objects/commands.ts index bc560f4..6db4bbf 100644 --- a/src/utils/objects/commands.ts +++ b/src/utils/objects/commands.ts @@ -1,10 +1,8 @@ -import { BotCommandsCategories } from '..' +import { TFunction } from '@sapphire/plugin-i18next' +import { BotCommandsCategories, languageKeys } from '..' -export const commandsCategoriesDescriptions: Record< - BotCommandsCategories, - string -> = { - games: ':game_die: GAMES :game_die:', - misc: ':tickets: MISCELLANEOUS :tickets:', - roles: ':jack_o_lantern: ROLES :jack_o_lantern: ', +export const commandsCategoriesDescriptions: Record string> = { + games: (t: TFunction) => t(languageKeys.categories.gamesCategoryDecoratedTitle), + misc: (t: TFunction) => t(languageKeys.categories.miscCategoryDecoratedTitle), + roles: (t: TFunction) => t(languageKeys.categories.rolesCategoryDecoratedTitle), } diff --git a/src/utils/objects/games/tictactoe.ts b/src/utils/objects/games/tictactoe.ts index 41d917d..6db048f 100644 --- a/src/utils/objects/games/tictactoe.ts +++ b/src/utils/objects/games/tictactoe.ts @@ -1,25 +1,19 @@ -import { GameFinalState, TicTacToeResultsParams } from '../..' +import { GameFinalState, languageKeys, TicTacToeResultsParams } from '../..' export const tttGameResults: Record< GameFinalState, (_: TicTacToeResultsParams) => { result: string; stopReason: string } > = { - [GameFinalState.PLAYER1_VICTORY]: ({ - player1, - player2, - }: TicTacToeResultsParams) => ({ - result: `:tada: CONGRATULATIONS ${player1}! You have won! :tada:`, + [GameFinalState.PLAYER1_VICTORY]: ({ player1, player2, t }: TicTacToeResultsParams) => ({ + result: t(languageKeys.commands.games.tictactoe.victoryResult, { username: player1.username }), stopReason: `${player1.username} won on TicTacToe against ${player2.username}!`, }), - [GameFinalState.PLAYER2_VICTORY]: ({ - player1, - player2, - }: TicTacToeResultsParams) => ({ - result: `:tada: CONGRATULATIONS ${player2}! You have won! :tada:`, + [GameFinalState.PLAYER2_VICTORY]: ({ player1, player2, t }: TicTacToeResultsParams) => ({ + result: t(languageKeys.commands.games.tictactoe.victoryResult, { username: player2.username }), stopReason: `${player2.username} won on TicTacToe against ${player1.username}!`, }), - [GameFinalState.TIE]: ({ player1, player2 }: TicTacToeResultsParams) => ({ - result: 'The game was a tie! :confetti_ball: Thanks for play (:', + [GameFinalState.TIE]: ({ player1, player2, t }: TicTacToeResultsParams) => ({ + result: t(languageKeys.commands.games.tictactoe.tieResult), stopReason: `Tictactoe game between ${player1.username} and ${player2.username} ends!`, }), [GameFinalState.UNDEFINED]: () => ({ result: '', stopReason: '' }), diff --git a/src/utils/objects/roles.ts b/src/utils/objects/roles.ts index 153c938..5ffcada 100644 --- a/src/utils/objects/roles.ts +++ b/src/utils/objects/roles.ts @@ -1,20 +1,15 @@ import { BotRoles } from '..' -export const roles: Record< - BotRoles, - { name: string; requiredPoints: number; imageUrl: string } -> = { +export const roles: Record = { [BotRoles.ZOTE]: { name: 'Zote', requiredPoints: 50, - imageUrl: - 'https://i.pinimg.com/originals/ee/03/46/ee034690129e85474178822972cc9694.gif', + imageUrl: 'https://i.pinimg.com/originals/ee/03/46/ee034690129e85474178822972cc9694.gif', }, [BotRoles.FALSE_KNIGHT]: { name: 'False Knight', requiredPoints: 250, - imageUrl: - 'https://64.media.tumblr.com/356b8e0e7dee00acc039604288194b3c/tumblr_px601eoSrB1wv5hmyo3_400.gif', + imageUrl: 'https://64.media.tumblr.com/356b8e0e7dee00acc039604288194b3c/tumblr_px601eoSrB1wv5hmyo3_400.gif', }, [BotRoles.HORNET]: { name: 'Hornet', @@ -24,61 +19,51 @@ export const roles: Record< [BotRoles.LOST_KIN]: { name: 'Lost Kin', requiredPoints: 1000, - imageUrl: - 'https://i.pinimg.com/originals/7f/ef/77/7fef776fab7c8e84fb0fe8923ac275ac.gif', + imageUrl: 'https://i.pinimg.com/originals/7f/ef/77/7fef776fab7c8e84fb0fe8923ac275ac.gif', }, [BotRoles.HIVE_KNIGHT]: { name: 'Hive Knight', requiredPoints: 2000, - imageUrl: - 'https://64.media.tumblr.com/a3fe65707da54f7b94fa2ef73300ad0e/tumblr_phxzts4UTK1wv5hmyo2_400.gif', + imageUrl: 'https://64.media.tumblr.com/a3fe65707da54f7b94fa2ef73300ad0e/tumblr_phxzts4UTK1wv5hmyo2_400.gif', }, [BotRoles.SOUL_MASTER]: { name: 'Soul Master', requiredPoints: 3000, - imageUrl: - 'https://cs9.pikabu.ru/post_img/2017/05/14/8/1494769148116435151.gif', + imageUrl: 'https://cs9.pikabu.ru/post_img/2017/05/14/8/1494769148116435151.gif', }, [BotRoles.WHITE_DEFENDER]: { name: 'White Defender', requiredPoints: 5000, - imageUrl: - 'https://64.media.tumblr.com/10cbee955cdc5787510ac0556832d6f8/tumblr_phxzts4UTK1wv5hmyo7_400.gif', + imageUrl: 'https://64.media.tumblr.com/10cbee955cdc5787510ac0556832d6f8/tumblr_phxzts4UTK1wv5hmyo7_400.gif', }, [BotRoles.GREAT_NAILSAGE_SLY]: { name: 'Great Nailsage Sly', requiredPoints: 8000, - imageUrl: - 'https://thumbs.gfycat.com/DefinitiveScientificHerring-max-1mb.gif', + imageUrl: 'https://thumbs.gfycat.com/DefinitiveScientificHerring-max-1mb.gif', }, [BotRoles.HOLLOW_KNIGHT]: { name: 'Hollow Knight', requiredPoints: 12000, - imageUrl: - 'https://i.pinimg.com/originals/0f/b6/67/0fb667fdaf03499eddff4829c14fa463.gif', + imageUrl: 'https://i.pinimg.com/originals/0f/b6/67/0fb667fdaf03499eddff4829c14fa463.gif', }, [BotRoles.THE_KNIGHT]: { name: 'The Knight', requiredPoints: 17000, - imageUrl: - 'https://media1.tenor.com/images/e055198dce05b168933a08bed1c39145/tenor.gif', + imageUrl: 'https://media1.tenor.com/images/e055198dce05b168933a08bed1c39145/tenor.gif', }, [BotRoles.NIGHTMARE_GRIMM]: { name: 'Nightmare Grimm', requiredPoints: 23000, - imageUrl: - 'https://i.pinimg.com/originals/cc/71/fc/cc71fc9af0932bee91a36ced6e9fcf93.gif', + imageUrl: 'https://i.pinimg.com/originals/cc/71/fc/cc71fc9af0932bee91a36ced6e9fcf93.gif', }, [BotRoles.PURE_VESSEL]: { name: 'Pure Vessel', requiredPoints: 30000, - imageUrl: - 'https://media1.tenor.com/images/cf6017c33f4a5a7f3e727d652ad93239/tenor.gif', + imageUrl: 'https://media1.tenor.com/images/cf6017c33f4a5a7f3e727d652ad93239/tenor.gif', }, [BotRoles.THE_ASCENDED_KNIGHT]: { name: 'The Ascended Knight', requiredPoints: 50000, - imageUrl: - 'https://thumbs.gfycat.com/FixedSmartJanenschia-size_restricted.gif', + imageUrl: 'https://thumbs.gfycat.com/FixedSmartJanenschia-size_restricted.gif', }, } diff --git a/src/utils/role.util.ts b/src/utils/role.util.ts index f23bdd5..22e3e83 100644 --- a/src/utils/role.util.ts +++ b/src/utils/role.util.ts @@ -2,27 +2,19 @@ import { Guild, GuildMember, Message, MessageEmbed, Role } from 'discord.js' import { logger } from './logger' import { configuration } from '@/config' import { utilLinks } from './links' -import { roles } from '.' +import { languageKeys, roles } from '.' +import { resolveKey } from '@sapphire/plugin-i18next' -export const ROLE_COLOR = configuration.embedMessageColor -export const CONTEXT = 'RoleUtil' +const ROLE_COLOR = configuration.client.embedMessageColor +const CONTEXT = 'RoleUtil' -export const defineRoles = ( - participationPoints: number, - user: GuildMember, - message: Message -) => { +export const defineRoles = (participationPoints: number, user: GuildMember, message: Message) => { const nextAvailableRole = getNextAvailableRoleFromUser(user) - if ( - nextAvailableRole && - participationPoints >= nextAvailableRole.requiredPoints - ) { + if (nextAvailableRole && participationPoints >= nextAvailableRole.requiredPoints) { // User accomplish requirements to gain a new role. const existingRoles = message.guild.roles.cache - const existingRoleInServer = existingRoles.find( - (role) => role.name === nextAvailableRole.name - ) + const existingRoleInServer = existingRoles.find((role) => role.name === nextAvailableRole.name) // Role has to exist on the server to be applied. if (existingRoleInServer) { const botRoleExistingInUser = getRoleFromUser(user) @@ -38,9 +30,7 @@ export const getRoleFromUser = (user: GuildMember): Role => { const userRoles = user.roles.cache const botRoleExistingInUser = userRoles.filter( - (userRole) => - Object.values(roles).find((role) => role.name === userRole.name) !== - undefined + (userRole) => Object.values(roles).find((role) => role.name === userRole.name) !== undefined ) return botRoleExistingInUser.first() @@ -57,9 +47,7 @@ export const getRole = ( return null } const availableBotRoles = Object.values(roles) - const role = availableBotRoles.find( - (botRole) => botRole.name === discordRole.name - ) + const role = availableBotRoles.find((botRole) => botRole.name === discordRole.name) return role } @@ -91,27 +79,26 @@ export const getNextAvailableRoleFromUser = ( return nextAvailableRole } -export const applyRole = async ( - role: Role, - previousRole: Role, - user: GuildMember, - message: Message -) => { +export const applyRole = async (role: Role, previousRole: Role, user: GuildMember, message: Message) => { try { if (previousRole) { await user.roles.remove(previousRole) } await user.roles.add(role) + const getNewRoleMsg = await resolveKey(message, languageKeys.rolesAssignment.userObtainsNewRole, { + username: user.user.username, + roleName: role.name, + }) const embedMessage = new MessageEmbed() .setColor(ROLE_COLOR) .setImage(utilLinks.roles.upgradeRole) - .setDescription( - `Congratulations ${user}, you have obtain the '${role.name}' role!` - ) + .setDescription(getNewRoleMsg) message.channel.send({ embeds: [embedMessage] }) } catch (error) { if (error.code === 50013) { - const errMessage = `Failed '${role.name}' role assignation. I guess I need more permissions ):` + const errMessage = await resolveKey(message, languageKeys.rolesAssignment.newRoleAssignmentError, { + roleName: role.name, + }) logger.error(errMessage, { context: CONTEXT }) message.channel.send(errMessage) } @@ -123,9 +110,7 @@ export const initRoles = async (message: Message): Promise => { const { guild } = message const botRoles = Object.values(roles) const rolesBuilder = botRoles.map(async (role) => { - if ( - !guild.roles.cache.find((guildRole) => guildRole.name === role.name) - ) { + if (!guild.roles.cache.find((guildRole) => guildRole.name === role.name)) { return guild.roles.create({ name: role.name, color: ROLE_COLOR, @@ -148,9 +133,7 @@ export const removeRoles = async (guild: Guild): Promise => { try { const botRoles = Object.values(roles) const rolesRemover = botRoles.map(async (role) => { - const existingRole = guild.roles.cache.find( - (guildRole: Role) => guildRole.name === role.name - ) + const existingRole = guild.roles.cache.find((guildRole: Role) => guildRole.name === role.name) if (existingRole) { return existingRole.delete('Bot fired from server') } @@ -158,10 +141,9 @@ export const removeRoles = async (guild: Guild): Promise => { await Promise.all(rolesRemover) return true } catch (error) { - logger.error( - `Authorization error. Does not have enough permissions on '${guild.name}' server to delete roles`, - { context: CONTEXT } - ) + logger.error(`Authorization error. Does not have enough permissions on '${guild.name}' server to delete roles`, { + context: CONTEXT, + }) } return false } diff --git a/src/utils/types/channel.ts b/src/utils/types/channel.ts index db19609..c74af6c 100644 --- a/src/utils/types/channel.ts +++ b/src/utils/types/channel.ts @@ -1,14 +1,3 @@ -import { - DMChannel, - PartialDMChannel, - NewsChannel, - TextChannel, - ThreadChannel, -} from 'discord.js' +import { DMChannel, PartialDMChannel, NewsChannel, TextChannel, ThreadChannel } from 'discord.js' -export type BotChannel = - | DMChannel - | PartialDMChannel - | NewsChannel - | TextChannel - | ThreadChannel +export type BotChannel = DMChannel | PartialDMChannel | NewsChannel | TextChannel | ThreadChannel diff --git a/src/utils/types/commands.ts b/src/utils/types/commands.ts index 23a619c..bb8531d 100644 --- a/src/utils/types/commands.ts +++ b/src/utils/types/commands.ts @@ -1,11 +1,6 @@ export type GameCommand = 'cancelGame' | 'tictactoe' | 'tictactoeleaderboard' -export type MiscCommand = - | 'congratulate' - | 'help' - | 'iniChibiKnight' - | 'say' - | 'shameonyou' +export type MiscCommand = 'congratulate' | 'help' | 'iniChibiKnight' | 'say' | 'shameonyou' export type RoleCommand = 'activateRoles' | 'myRole' | 'roles' diff --git a/src/utils/types/games/tictactoe.ts b/src/utils/types/games/tictactoe.ts index 74231e2..b49ae83 100644 --- a/src/utils/types/games/tictactoe.ts +++ b/src/utils/types/games/tictactoe.ts @@ -1,14 +1,17 @@ +import { TFunction } from '@sapphire/plugin-i18next' import { Message, User } from 'discord.js' export type TicTacToeMoveResolverParams = { message: Message player1?: User player2: User + t: TFunction } export type TicTacToeResultsParams = { player1: User player2: User + t: TFunction } export enum TicTacToeButtonId { diff --git a/src/utils/types/index.ts b/src/utils/types/index.ts index c7b42af..7c82a5e 100644 --- a/src/utils/types/index.ts +++ b/src/utils/types/index.ts @@ -1,5 +1,6 @@ export * from './games' export * from './channel' export * from './commands' +export * from './preconditions' export * from './roles' export * from './user' diff --git a/src/utils/types/preconditions.ts b/src/utils/types/preconditions.ts new file mode 100644 index 0000000..b2dc9af --- /dev/null +++ b/src/utils/types/preconditions.ts @@ -0,0 +1,20 @@ +export enum CustomPrecondition { + RolesDeactivatedOnly = 'RolesDeactivatedOnly', + RolesActivatedOnly = 'RolesActivatedOnly', + BotNotInitializedOnly = 'BotNotInitializedOnly', + BotInitializedOnly = 'BotInitializedOnly', + AdminOnly = 'AdminOnly', +} + +export interface ICustomPreconditions { + /** Roles */ + [CustomPrecondition.RolesDeactivatedOnly]: never + [CustomPrecondition.RolesActivatedOnly]: never + + /** Server */ + [CustomPrecondition.BotNotInitializedOnly]: never + [CustomPrecondition.BotInitializedOnly]: never + + /** User */ + [CustomPrecondition.AdminOnly]: never +} diff --git a/src/utils/types/roles.ts b/src/utils/types/roles.ts index 7b5b978..6bc5284 100644 --- a/src/utils/types/roles.ts +++ b/src/utils/types/roles.ts @@ -1,3 +1,4 @@ +import { TFunction } from '@sapphire/plugin-i18next' import { CacheType, Message, MessageComponentInteraction } from 'discord.js' import { BotCommandsCategories } from '.' @@ -30,5 +31,6 @@ export enum RolesButtonId { export type ActivateRolesResolverParams = { message: Message + t: TFunction interaction?: MessageComponentInteraction } diff --git a/src/utils/types/types.d.ts b/src/utils/types/types.d.ts index 94f1a7e..4d358f0 100644 --- a/src/utils/types/types.d.ts +++ b/src/utils/types/types.d.ts @@ -1,4 +1,5 @@ import { MongoDatabase, Cache } from '@/database' +import { ICustomPreconditions } from './preconditions' declare module '@sapphire/pieces' { interface Container { @@ -8,16 +9,5 @@ declare module '@sapphire/pieces' { } declare module '@sapphire/framework' { - interface Preconditions { - /** Roles */ - RolesNotActiveOnly: never - RolesActiveOnly: never - - /** Server */ - BotNotInitializeOnly: never - BotInitializeOnly: never - - /** User */ - AdminOnly: never - } + interface Preconditions extends ICustomPreconditions {} } diff --git a/tsconfig.json b/tsconfig.json index fcce922..8e86005 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "lib": [ "es2021" ], + "resolveJsonModule": true, }, - "include": ["src/**/*"] + "include": ["src/**/*", "src/**/*.json"] } diff --git a/yarn.lock b/yarn.lock index fe4698a..273a467 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/runtime@^7.12.0": + version "7.16.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.5.tgz#7f3e34bf8bdbbadf03fbb7b1ea0d929569c9487a" + integrity sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA== + dependencies: + regenerator-runtime "^0.13.4" + "@discordjs/builders@^0.10.0": version "0.10.0" resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-0.10.0.tgz#5a5d0927c84564e48093761a1b4d385c7148e4c8" @@ -117,6 +124,17 @@ "@sapphire/utilities" "^3.1.0" tslib "^2.3.1" +"@sapphire/plugin-i18next@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@sapphire/plugin-i18next/-/plugin-i18next-2.1.4.tgz#1af6e5644a95142eba68cdd2d3f77deb3a5d5ef5" + integrity sha512-gKjkvgvgBrjBi/Z67QipauSADKneqq2SrrkvENXDvZtY3PpjQxAh6T4x7P8RQOWQVPJL6NCNyDgErs4nrn5J3A== + dependencies: + "@sapphire/utilities" "^3.1.0" + "@types/i18next-fs-backend" "^1.1.2" + i18next "^21.6.4" + i18next-fs-backend "^1.1.4" + tslib "^2.3.1" + "@sapphire/plugin-logger@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@sapphire/plugin-logger/-/plugin-logger-2.1.1.tgz#0dbb0dee203a97cc5cc6b2a1c33d1368a78e3735" @@ -161,6 +179,13 @@ semver "^7.3.2" tslib "^2.3.1" +"@types/i18next-fs-backend@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/i18next-fs-backend/-/i18next-fs-backend-1.1.2.tgz#4f3116769229371fcdf64bbdb6841ea745e392f9" + integrity sha512-ZzTRXA5B0x0oGhzKNp08IsYjZpli4LjRZpg3q4j0XFxN5lKG2MVLnR4yHX8PPExBk4sj9Yfk1z9O6CjPrAlmIQ== + dependencies: + i18next "^21.0.1" + "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz" @@ -1198,6 +1223,18 @@ husky@^7.0.4: resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535" integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ== +i18next-fs-backend@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-1.1.4.tgz#d0e9b9ed2fa7a0f11002d82b9fa69c3c3d6482da" + integrity sha512-/MfAGMP0jHonV966uFf9PkWWuDjPYLIcsipnSO3NxpNtAgRUKLTwvm85fEmsF6hGeu0zbZiCQ3W74jwO6K9uXA== + +i18next@^21.0.1, i18next@^21.6.4: + version "21.6.4" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.4.tgz#4da52a429094112b33d4383b5f44aa4890dcb081" + integrity sha512-pk0Utxtq5g//Q9ONQ0w3+PRSwOFJ7+m4rAekhHHg1Xql0KcUjJU7DzmzweGy6DsrIQ1GM/t57wLeeGuyGKtjTg== + dependencies: + "@babel/runtime" "^7.12.0" + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" @@ -1862,6 +1899,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regexp-clone@1.0.0, regexp-clone@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz"