diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index c69ebea27..5acc77883 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,10 +29,10 @@ jobs: with: languages: ${{ matrix.language }} - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v5 with: - java-version: '21' + java-version: '25' distribution: 'temurin' cache: gradle diff --git a/.github/workflows/reusable-gradle-build.yml b/.github/workflows/reusable-gradle-build.yml index ecd0e049a..d0e9e8cf8 100644 --- a/.github/workflows/reusable-gradle-build.yml +++ b/.github/workflows/reusable-gradle-build.yml @@ -6,11 +6,11 @@ on: gradle-version: required: false type: string - default: '8.14.3' + default: '9.4.0' java-version: required: false type: string - default: '21' + default: '25' jobs: gradle-build: diff --git a/.github/workflows/reusable-gradle-test.yml b/.github/workflows/reusable-gradle-test.yml index a850aadb3..83f4bfb49 100644 --- a/.github/workflows/reusable-gradle-test.yml +++ b/.github/workflows/reusable-gradle-test.yml @@ -6,11 +6,11 @@ on: java-version: required: false type: string - default: '21' + default: '25' gradle-version: required: false type: string - default: '8.14.3' + default: '9.4.0' secrets: FORGEJO_REGISTRY: required: true @@ -49,7 +49,7 @@ jobs: run: docker compose -f docker-compose.test.yml up -d --wait - name: Test with Gradle - run: gradle test + run: gradle clean test env: CAFEBOT_API_URL: 'http://localhost:5000' diff --git a/Dockerfile b/Dockerfile index 11c2c2341..cad6e6216 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Use an official Gradle image with JDK 21 for building -FROM gradle:jdk21 AS build +FROM gradle:jdk25 AS build # Set the working directory WORKDIR /app diff --git a/build.gradle.kts b/build.gradle.kts index 5aa89b2db..3df970dea 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,8 +21,8 @@ allprojects { apply(plugin = "com.gradleup.shadow") java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 } repositories { @@ -100,6 +100,7 @@ tasks.clean { dependencies { implementation(project(":modules:cafeBot-api-wrapper")) implementation(project(":modules:meme-api-wrapper")) + implementation(project(":modules:i18n")) implementation("net.dv8tion:JDA:6.4.0") { exclude(module = "opus-java") } diff --git a/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/server/PollApiTest.java b/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/server/PollApiTest.java index 976940603..410078a4d 100644 --- a/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/server/PollApiTest.java +++ b/modules/cafeBot-api-wrapper/src/test/java/com/beanbeanjuice/cafebot/endpoints/discord/server/PollApiTest.java @@ -17,11 +17,7 @@ public class PollApiTest extends ApiTest { - private Poll poll; - private String user1; - - @BeforeEach - public void setup() throws ExecutionException, InterruptedException { + private static Poll setupPoll() throws ExecutionException, InterruptedException { String guildId = generateSnowflake().toString(); String messageId = generateSnowflake().toString(); String user1 = generateSnowflake().toString(); @@ -35,32 +31,36 @@ public void setup() throws ExecutionException, InterruptedException { "Example Title", "Example Description", true, - Instant.now().plus(2, ChronoUnit.SECONDS).toString(), + Instant.now().plus(1, ChronoUnit.SECONDS).toString(), options.toArray(new PartialPollOption[0]) ); - this.poll = cafeAPI.getPollApi().createPoll(guildId, messageId, partialPoll).get(); - this.poll = cafeAPI.getPollApi().toggleVote(this.poll.getId(), this.poll.getOptions()[0].getId(), user1).get(); + Poll poll = cafeAPI.getPollApi().createPoll(guildId, messageId, partialPoll).get(); + poll = cafeAPI.getPollApi().toggleVote(poll.getId(), poll.getOptions()[0].getId(), user1).get(); + + Thread.sleep(Duration.of(1, ChronoUnit.SECONDS).toMillis()); + + return poll; } @Test @DisplayName("can get all polls") public void testCanGetAllPolls() throws ExecutionException, InterruptedException { - Thread.sleep(Duration.of(3, ChronoUnit.SECONDS).toMillis()); + Poll originalPoll = setupPoll(); Map> polls = cafeAPI.getPollApi().getPolls().get(); - Assertions.assertNotNull(polls.get(this.poll.getGuildId())); - Assertions.assertEquals(1, polls.get(this.poll.getGuildId()).size()); - Assertions.assertNotNull(polls.get(this.poll.getGuildId()).getFirst()); + Assertions.assertNotNull(polls.get(originalPoll.getGuildId())); + Assertions.assertEquals(1, polls.get(originalPoll.getGuildId()).size()); + Assertions.assertNotNull(polls.get(originalPoll.getGuildId()).getFirst()); } @Test @DisplayName("can get polls for guild") public void testCanGetSpecificPoll() throws ExecutionException, InterruptedException { - Thread.sleep(Duration.of(3, ChronoUnit.SECONDS).toMillis()); + Poll originalPoll = setupPoll(); - List polls = cafeAPI.getPollApi().getPolls(this.poll.getGuildId()).get(); + List polls = cafeAPI.getPollApi().getPolls(originalPoll.getGuildId()).get(); Assertions.assertEquals(1, polls.size()); Assertions.assertNotNull(polls.getFirst()); @@ -98,7 +98,7 @@ public void testCanCreatePoll() throws ExecutionException, InterruptedException Assertions.assertTrue(poll.getDescription().isPresent()); Assertions.assertEquals("Example Description", poll.getDescription().get()); - Assertions.assertEquals(2, poll.getOptions()[0].getEmoji().get().length()); + Assertions.assertEquals("🥺".length(), poll.getOptions()[0].getEmoji().get().length()); Assertions.assertEquals("🥺", poll.getOptions()[0].getEmoji().get()); Assertions.assertEquals("Poll Option #1", poll.getOptions()[0].getTitle()); Assertions.assertTrue(poll.getOptions()[0].getDescription().isPresent()); @@ -112,9 +112,10 @@ public void testCanCreatePoll() throws ExecutionException, InterruptedException @Test @DisplayName("can vote for poll") public void testCanVoteForPoll() throws ExecutionException, InterruptedException { + Poll originalPoll = setupPoll(); String user = generateSnowflake().toString(); - Poll poll = cafeAPI.getPollApi().toggleVote(this.poll.getId(), this.poll.getOptions()[1].getId(), user).get(); + Poll poll = cafeAPI.getPollApi().toggleVote(originalPoll.getId(), originalPoll.getOptions()[1].getId(), user).get(); Assertions.assertNotNull(poll); Assertions.assertTrue(Arrays.stream(poll.getOptions()[1].getVoters()).anyMatch((voters) -> voters.equalsIgnoreCase(user))); @@ -123,9 +124,9 @@ public void testCanVoteForPoll() throws ExecutionException, InterruptedException @Test @DisplayName("can close poll") public void testCanClosePoll() throws ExecutionException, InterruptedException { - Thread.sleep(Duration.of(3, ChronoUnit.SECONDS).toMillis()); + Poll originalPoll = setupPoll(); - Poll poll = cafeAPI.getPollApi().closePoll(this.poll.getId()).get(); + Poll poll = cafeAPI.getPollApi().closePoll(originalPoll.getId()).get(); Assertions.assertNotNull(poll); Assertions.assertFalse(poll.isActive()); @@ -135,33 +136,37 @@ public void testCanClosePoll() throws ExecutionException, InterruptedException { @Test @DisplayName("can delete poll") public void testCanDeletePoll() throws ExecutionException, InterruptedException { - List polls = cafeAPI.getPollApi().getPolls(this.poll.getGuildId(), true, false).get(); + Poll originalPoll = setupPoll(); + + List polls = cafeAPI.getPollApi().getPolls(originalPoll.getGuildId(), true, false).get(); Assertions.assertEquals(1, polls.size()); - cafeAPI.getPollApi().deletePoll(this.poll.getId()).join(); + cafeAPI.getPollApi().deletePoll(originalPoll.getId()).join(); - polls = cafeAPI.getPollApi().getPolls(this.poll.getGuildId()).get(); + polls = cafeAPI.getPollApi().getPolls(originalPoll.getGuildId()).get(); Assertions.assertEquals(0, polls.size()); } @Test @DisplayName("can get poll by message id") public void testCanGetPollById() throws ExecutionException, InterruptedException { - Poll poll = cafeAPI.getPollApi().getPoll(this.poll.getGuildId(), this.poll.getMessageId()).get(); + Poll originalPoll = setupPoll(); + Poll poll = cafeAPI.getPollApi().getPoll(originalPoll.getGuildId(), originalPoll.getMessageId()).get(); Assertions.assertNotNull(poll); - Assertions.assertEquals(poll.getId(), this.poll.getId()); - Assertions.assertEquals(poll.getTitle(), this.poll.getTitle()); - Assertions.assertEquals(poll.getDescription(), this.poll.getDescription()); + Assertions.assertEquals(poll.getId(), originalPoll.getId()); + Assertions.assertEquals(poll.getTitle(), originalPoll.getTitle()); + Assertions.assertEquals(poll.getDescription(), originalPoll.getDescription()); } @Test @DisplayName("can manually set poll submission to false") public void testCanSetPollSubmissionToFalse() throws ExecutionException, InterruptedException { - int originalVotes = poll.getOptions()[0].getVoters().length; + Poll originalPoll = setupPoll(); + int originalVotes = originalPoll.getOptions()[0].getVoters().length; String user = generateSnowflake().toString(); - Poll poll = cafeAPI.getPollApi().setVote(this.poll.getId(), this.poll.getOptions()[0].getId(), user, true).get(); + Poll poll = cafeAPI.getPollApi().setVote(originalPoll.getId(), originalPoll.getOptions()[0].getId(), user, true).get(); Assertions.assertEquals(originalVotes + 1, poll.getOptions()[0].getVoters().length); } @@ -169,10 +174,11 @@ public void testCanSetPollSubmissionToFalse() throws ExecutionException, Interru @Test @DisplayName("can manually set poll submission to true") public void testCanSetPollSubmissionToTrue() throws ExecutionException, InterruptedException { - int originalVotes = poll.getOptions()[0].getVoters().length; + Poll originalPoll = setupPoll(); + int originalVotes = originalPoll.getOptions()[0].getVoters().length; String user = generateSnowflake().toString(); - Poll poll = cafeAPI.getPollApi().setVote(this.poll.getId(), this.poll.getOptions()[0].getId(), user, false).get(); + Poll poll = cafeAPI.getPollApi().setVote(originalPoll.getId(), originalPoll.getOptions()[0].getId(), user, false).get(); Assertions.assertEquals(originalVotes, poll.getOptions()[0].getVoters().length); } diff --git a/modules/i18n/build.gradle.kts b/modules/i18n/build.gradle.kts new file mode 100644 index 000000000..f5989db53 --- /dev/null +++ b/modules/i18n/build.gradle.kts @@ -0,0 +1,9 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +version = "0.0.0" + +dependencies { + implementation("org.yaml:snakeyaml:2.5") // https://mvnrepository.com/artifact/org.yaml/snakeyaml +} + +tasks.withType { } diff --git a/modules/i18n/src/main/java/com/beanbeanjuice/cafebot/i18n/I18N.java b/modules/i18n/src/main/java/com/beanbeanjuice/cafebot/i18n/I18N.java new file mode 100644 index 000000000..86858a9e5 --- /dev/null +++ b/modules/i18n/src/main/java/com/beanbeanjuice/cafebot/i18n/I18N.java @@ -0,0 +1,187 @@ +package com.beanbeanjuice.cafebot.i18n; + +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class I18N { + + private final Locale locale; + + private static final Map> YAML_CACHE = new ConcurrentHashMap<>(); + private static final Map> FILE_PATH_CACHE = new ConcurrentHashMap<>(); + + public I18N() { + this.locale = Locale.ENGLISH; + } + + public I18N(Locale locale) { + this.locale = locale; + } + + public String getString(String key) { + return getString(key, this.locale); + } + + public List getStringArray(String key) { + return getStringArray(key, this.locale); + } + + private static String getString(String key, Locale locale) { + // There are 2 paths we need to find. First, the path to the file. Next, the path within the file. + // For example, i18n/en/command/ai.yml, and command.ai.description + // Then we know command.ai is the file path, and description is the path within the file. + // Question is... how do we do this? There might be a dynamic number of folders. + Optional filePath = findFilePath(key, locale) + .or(() -> findFilePath(key, Locale.ENGLISH)); + + if (filePath.isEmpty()) return key; + + /* + Now we need to remove the overlap from the key and the filePath. + For example, the filePath might be i18n/en/command/ai.yml and + the key might be command.ai.description + + This means we need to remove "command.ai." + */ + + String pathInFile = parseKey(filePath.get(), key); + Map map = loadYaml(filePath.get()); + + if (map.isEmpty()) return key; + + Optional flattenedString = getFlattenedString(map, pathInFile); + + // If the string wasn't found, and the current locale is English, then it truly does not exist. + if (flattenedString.isEmpty() && locale.equals(Locale.ENGLISH)) return key; + + return flattenedString.orElseGet(() -> getString(key, Locale.ENGLISH)); + } + + private static List getStringArray(String key, Locale locale) { + Optional filePath = findFilePath(key, locale) + .or(() -> findFilePath(key, Locale.ENGLISH)); + + if (filePath.isEmpty()) return new ArrayList<>(); + + /* + Now we need to remove the overlap from the key and the filePath. + For example, the filePath might be i18n/en/command/ai.yml and + the key might be command.ai.description + + This means we need to remove "command.ai." + */ + + String pathInFile = parseKey(filePath.get(), key); + Map map = loadYaml(filePath.get()); + if (map.isEmpty()) return new ArrayList<>(); + + List flattenedStringArray = getFlattenedStringArray(map, pathInFile); + + // If the string wasn't found, and the current locale is English, then it truly does not exist. + if (flattenedStringArray.isEmpty() && locale.equals(Locale.ENGLISH)) return flattenedStringArray; + + if (!flattenedStringArray.isEmpty()) return flattenedStringArray; + return getStringArray(key, Locale.ENGLISH); + } + + @SuppressWarnings("unchecked") + private static Optional getFlattenedString(Map map, String key) { + String[] split = key.split("\\."); + + for (int i = 0; i < split.length - 1; i++) { + if (!map.containsKey(split[i])) return Optional.empty(); + + map = (Map) map.get(split[i]); + } + + String finalKey = split[split.length - 1]; + return (map.containsKey(finalKey)) ? ((String) map.get(finalKey)).describeConstable() : Optional.empty(); + } + + @SuppressWarnings("unchecked") + private static List getFlattenedStringArray(Map map, String key) { + String[] split = key.split("\\."); + + for (int i = 0; i < split.length - 1; i++) { + if (!map.containsKey(split[i])) return new ArrayList<>(); + + map = (Map) map.get(split[i]); + } + + String finalKey = split[split.length - 1]; + return (map.containsKey(finalKey)) ? ((List) map.get(finalKey)) : new ArrayList<>(); + } + + private static String parseKey(String filePath, String key) { + String[] splitFilePath = filePath // i18n/en/command/ai.yml + .replace(".yml", "") // i18n/en/command/ai + .split("/"); // ["i18n", "en", "command", "ai"] + + String[] splitKey = key.split("\\."); // ["ai", "embed", "title"] + + // Two pointer approach + int fileIndex = 0; + int keyIndex = 0; + + while (fileIndex < splitFilePath.length && keyIndex < splitKey.length) { + if (!splitFilePath[fileIndex].equals(splitKey[keyIndex])) { + fileIndex++; + continue; + } + + fileIndex++; + keyIndex++; + } + + // Key starts at keyIndex + return String.join(".", Arrays.copyOfRange(splitKey, keyIndex, splitKey.length)); + } + + private static Optional findFilePathUncached(String key, Locale locale) { + StringBuilder sb = new StringBuilder(); + sb.append("i18n/").append(locale.toString()).append("/"); // i18n/en/ + + String[] splitKey = key.split("\\."); + + for (String split : splitKey) { + sb.append(split); // i18n/en/command + + URL resource = I18N.class.getClassLoader().getResource(sb.toString()); + if (resource == null) { + // Directory with that name not found. Does a .yml exist in current directory? + resource = I18N.class.getClassLoader().getResource(sb + ".yml"); + if (resource == null && locale.equals(Locale.ENGLISH)) return Optional.empty(); // if english, and not found, it doesn't exist. + if (resource == null) return findFilePath(key, Locale.ENGLISH); // default to english. + return Optional.of(sb.append(".yml").toString()); + } + + sb.append("/"); + } + + return Optional.empty(); + } + + private static Optional findFilePath(String key, Locale locale) { + String cacheKey = locale + ":" + key; + return FILE_PATH_CACHE.computeIfAbsent(cacheKey, k -> findFilePathUncached(key, locale)); + } + + private static Map loadYaml(String filePath) { + return YAML_CACHE.computeIfAbsent(filePath, path -> { + try (InputStream is = I18N.class.getClassLoader().getResourceAsStream(path)) { + return new Yaml().load(is); + } catch (IOException e) { return Map.of(); } + }); + } + +} diff --git a/modules/i18n/src/main/resources/i18n/en/command/ai.yml b/modules/i18n/src/main/resources/i18n/en/command/ai.yml new file mode 100644 index 000000000..1bab2062f --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/ai.yml @@ -0,0 +1,11 @@ +description: "Want a sassy AI that will serve you some coffee?" +embed: + title: "AI Response Changed" + enabled: "Status: Enabled ✅" + disabled: "Status: Disabled ❌" +error: + title: "Error Changing AI Response" + description: "There was an error changing the AI response... I'm so sorry..." +arguments: + status: + description: "Whether to enable or disable the \"AI\"." diff --git a/modules/i18n/src/main/resources/i18n/en/command/avatar.yml b/modules/i18n/src/main/resources/i18n/en/command/avatar.yml new file mode 100644 index 000000000..30913c6aa --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/avatar.yml @@ -0,0 +1,15 @@ +description: "Get someone's avatar!" +embed: + title: "{user}'s Avatar" +error: + missing: + title: "No User Avatar" + description: "The specified user does not have a Discord avatar." + server: + title: "Must Be In Server" + description: "You must be in a Discord server to get a server avatar!" +arguments: + user: + description: "Get their user avatar!" + server: + description: "Get their server avatar!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/balance.yml b/modules/i18n/src/main/resources/i18n/en/command/balance.yml new file mode 100644 index 000000000..3125ce53d --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/balance.yml @@ -0,0 +1,14 @@ +description: "Get your balance!" +error: + title: "Error Getting Balance" + description: "There was some sort of error getting your coin balance." +embed: + title: "cafeCoin Balance" + orders: + bought: "Orders Bought" + received: "Orders Received" + description: "{user} has a current balance of `{balance}` cC (cafeCoins)!" + footer: "To learn how to make money, do /help!" +arguments: + user: + description: "The user you want to get the balance of." diff --git a/modules/i18n/src/main/resources/i18n/en/command/banner.yml b/modules/i18n/src/main/resources/i18n/en/command/banner.yml new file mode 100644 index 000000000..fd8cd1941 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/banner.yml @@ -0,0 +1,9 @@ +description: "Get someone's banner!" +embed: + title: "{user}'s Banner" +error: + title: "No User Banner" + description: "The specified user does not have a Discord banner." +arguments: + user: + description: "Get their banner!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/birthday.yml b/modules/i18n/src/main/resources/i18n/en/command/birthday.yml new file mode 100644 index 000000000..994b20ce1 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/birthday.yml @@ -0,0 +1,42 @@ +description: "Get someone's birthday or change your own!" +subcommand: + get: + description: "Get someone's birthday!" + embed: + other: + title: "🎂 {user}'s Birthday" + description: "Their birthday is on **{month} {day}** ({timezone})." + self: + title: "🎂 Your Birthday" + description: "Your birthday is on **{month} {day}** ({timezone})." + error: + title: "Error Getting Birthday" + description: "<:cafeBot_sad:1171726165040447518> Sorry... there was an error getting their birthday. It might not be set, you should tell them to set it with `/birthday`!" + arguments: + user: + description: "The user who's birthday you want to see." + + set: + description: "Edit your birthday!" + embed: + error: + title: "Error Setting Birthday" + description: "I... I don't know what happened... the computer's not letting me put your birthday in!" + success: + title: "🎂 Birthday Set" + description: "You have successfully set your birthday to **{month} {day}** ({timezone})." + arguments: + month: + description: "The month you were born!" + day: + description: "The day you were born!" + timezone: + description: "Your current timezone! Start typing to see available options." + year: + description: "The year you were born in!" + + remove: + description: "Remove your birthday... :c" + embed: + title: "Birthday Removed 🥺" + description: "<:cafeBot_sad:1171726165040447518> Your birthday has been removed... but I know it's sometimes better to keep things private..." diff --git a/modules/i18n/src/main/resources/i18n/en/command/bot-donate.yml b/modules/i18n/src/main/resources/i18n/en/command/bot-donate.yml new file mode 100644 index 000000000..8aad1a588 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/bot-donate.yml @@ -0,0 +1,8 @@ +description: "Donate to keep the bot up!" + +success: + title: "Donations!" + description: | + Donations are absolutely optional, but they help keep me alive! + You can donate [here](https://buymeacoffee.com/beanbeanjuice)! + Thank you so much... diff --git a/modules/i18n/src/main/resources/i18n/en/command/bot-invite.yml b/modules/i18n/src/main/resources/i18n/en/command/bot-invite.yml new file mode 100644 index 000000000..f259bcdfd --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/bot-invite.yml @@ -0,0 +1 @@ +description: "Want to invite this bot to a server? Use this command!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/bot-upvote.yml b/modules/i18n/src/main/resources/i18n/en/command/bot-upvote.yml new file mode 100644 index 000000000..bfced85c7 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/bot-upvote.yml @@ -0,0 +1,8 @@ +description: "Upvote the bot!" + +embed: + title: "Voting List" + description: | + Upvoting helps me serve coffee to more people! + If you're enjoying my service, please consider doing so! + Any and all support is welcome! diff --git a/modules/i18n/src/main/resources/i18n/en/command/bug.yml b/modules/i18n/src/main/resources/i18n/en/command/bug.yml new file mode 100644 index 000000000..c0d3fd99d --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/bug.yml @@ -0,0 +1,3 @@ +description: "A bug?!?! Where?!?!" +button: + label: "Bug Report" diff --git a/modules/i18n/src/main/resources/i18n/en/command/calendar.yml b/modules/i18n/src/main/resources/i18n/en/command/calendar.yml new file mode 100644 index 000000000..d330e0ddf --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/calendar.yml @@ -0,0 +1,62 @@ +description: "All things to do with calendars!" + +subcommands: + add: + description: "Add a calendar!" + success: + title: "Calendar Added!" + description: "Successfully added your calendar! Use `/calender get` to preview your calendar!" + error: + count: + title: "Too Many Calendars!" + description: "You have added too many calendars... You can only have 3 active ones!" + url: + title: "Invalid URL" + description: "You must use a *valid* calendar URL!" + server: + title: "Invalid Command!" + description: "In order to set a server calendar, you need to use this command in a Discord server!" + generic: + title: "Error Adding Calendar" + description: "I'm sorry... I don't know *what* went wrong..." + arguments: + type: + description: "The type of calendar you want to add." + name: + description: "The calendar name!" + url: + description: "The calendar url! Make sure it ends in \".ics\"!" + + get: + description: "Get your calendars!" + error: + title: "Error Getting Calendar" + description: "I... couldn't find the calendar... is this an error??" + arguments: + id: + description: "The ID of the calendar you want to view!" + timezone: + description: "The timezone you want the calendar in." + + list: + description: "List all of the calendars for a specific user!" + success: + title: "Calendar List" + footer: "You can view guild calendars by typing /calendar get!" + empty: "No calendars found!" + arguments: + user: + description: "The user you want to see the calendar of!" + + delete: + description: "Delete a calendar!" + error: + exploit: + title: "You're joking right?" + description: "Oh well.. I thought you were smarter than that... since you tried to use an exploit I made sure to talk to my boss! <:cafeBot_angry:1171726164092518441>" + success: + title: "Calendar Deleted!" + description: "We won't see that pesky calendar any longer!" + arguments: + id: + description: "The ID of the calendar you want to delete!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/coinflip.yml b/modules/i18n/src/main/resources/i18n/en/command/coinflip.yml new file mode 100644 index 000000000..dde236e04 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/coinflip.yml @@ -0,0 +1,6 @@ +description: "Flip a coin!" +coin: + heads: "HEADS" + tails: "TAILS" +embed: + description: "The coin is **{side}**!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/counting-statistics.yml b/modules/i18n/src/main/resources/i18n/en/command/counting-statistics.yml new file mode 100644 index 000000000..8e8a1edb1 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/counting-statistics.yml @@ -0,0 +1,17 @@ +description: "Get the server's global counting statistics!" +embed: + stats: + title: "Global Counting Statistics ➕" + description: | + ***Highest Ranking***: {highest}/{num_guilds} + *This is the ranking for your* ***highest*** *number.* + + ***Current Ranking***: {current}/{num_guilds} + *This is the ranking for your* ***current*** *number.* + extended-description: | + + Your highest number is **{highest}**! + Your current number is **{current}**! This means your next number should be **{next}**... + error: + title: "Counting Statistics Error" + description: "I... I'm really sorry... I'm having trouble getting the global counting statistics..." diff --git a/modules/i18n/src/main/resources/i18n/en/command/define.yml b/modules/i18n/src/main/resources/i18n/en/command/define.yml new file mode 100644 index 000000000..7bc40e0ea --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/define.yml @@ -0,0 +1,12 @@ +description: "Define something!" + +error: + unknown: + title: "Unknown Word" + description: "<:cafeBot_sad:1171726165040447518> I-... I don't know that word... I swear I'll learn it someday!" + +arguments: + word: + description: "The word to define." + language_code: + description: "The language code to define." diff --git a/modules/i18n/src/main/resources/i18n/en/command/donate.yml b/modules/i18n/src/main/resources/i18n/en/command/donate.yml new file mode 100644 index 000000000..aa54fe1e5 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/donate.yml @@ -0,0 +1,28 @@ +description: "Donate your cafeCoins to another user!" +embed: + donation: + title: "Money up!" + description: | + Aww!~ + + {donator} just donated **{amount} bC** to {donatee}! + footer: "If you want to donate too, do \"/donate!\"" + cooldown: + title: "Donation Cooldown" + description: That user was last donated to at . You need to wait before they are able to receive another donation. + footer: "They can only be donated to once per hour!" + balance: + title: "Not Enough Cafe Coins!" + description: | + Umm.. you only have {balance} which is *not* enough to donate {payment}. + + If you don't have money to pay then why are you even trying to donate? Do you even have money to order from the store? Ugh... I guess I might give you some money if you serve some customers... + footer: "To get some money, do \"/serve\"!" + self: + title: "Self Donation" + description: "You're joking... right? You can't donate to yourself. <:cafeBot_angry:1171726164092518441>" +arguments: + user: + description: "The user you want to donate to." + amount: + description: "The amount of cafeCoins you want to donate!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/eightball.yml b/modules/i18n/src/main/resources/i18n/en/command/eightball.yml new file mode 100644 index 000000000..ba643d371 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/eightball.yml @@ -0,0 +1,24 @@ +description: "Have a question? Well I have an answer..." +embed: + field_title: "My Mystical Answer" + field_description: "*{answer}*" +answers: + positive: + - "It is likely..." + - "Probably." + - "Without a doubt." + - "Of course." + - "More than likely." + - "YES ABSOLUTELY." + - "Hmmm... I think so." + negative: + - "Of course not." + - "Probably not." + - "It is not likely..." + - "There is some doubt..." + - "Less than likely." + - "ABSOLUTELY NOT." + - "Are you kidding? No!" +arguments: + question: + description: "The question you want to ask me!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/embed.yml b/modules/i18n/src/main/resources/i18n/en/command/embed.yml new file mode 100644 index 000000000..7422fe8cd --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/embed.yml @@ -0,0 +1,27 @@ +description: "Create a beautiful embed!" + +success: + title: "Embed Created" + description: "Your embed has been successfully created in {channel}!" + +error: + attachment: + title: "Invalid Attachments" + description: "Attachments must be images." + channel: + title: "Invalid Channel" + description: "{channel} is not a text channel." + invalid: + title: "Invalid Embed" + description: "You're trying to create an invalid embed... I'm so sorry I can't let you do that..." + +arguments: + channel: "The channel to send the embed in." + title: "Title for the embed." + description: "The message that goes INSIDE the embed." + author: "The author of the embed." + message: "The message that goes outside of the embed." + footer: "A message to add at the bottom of the embed." + thumbnail: "A thumbnail url to add to the embed." + image: "An image url to add to the image." + color: "Color hex code. Example: #FFC0CB" diff --git a/modules/i18n/src/main/resources/i18n/en/command/feature.yml b/modules/i18n/src/main/resources/i18n/en/command/feature.yml new file mode 100644 index 000000000..6177026d0 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/feature.yml @@ -0,0 +1 @@ +description: "Request a new feature for me!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/game.yml b/modules/i18n/src/main/resources/i18n/en/command/game.yml new file mode 100644 index 000000000..b96c1b512 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/game.yml @@ -0,0 +1,15 @@ +description: "All the games you could ever need!" + +subcommands: + stats: + description: "Get someone's game stats!" + embed: + title: "{user}'s Game Statistics" + fields: + total: + name: "**Totals** 🎯" + stats: "**Wins**: {wins}\n**Losses**: {losses}\n**Played**: {played}" + footer: "Do you want to play a game?~" + arguments: + user: + description: "The person who's game data you want to see." diff --git a/modules/i18n/src/main/resources/i18n/en/command/meme.yml b/modules/i18n/src/main/resources/i18n/en/command/meme.yml new file mode 100644 index 000000000..205b033d4 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/meme.yml @@ -0,0 +1,12 @@ +description: "Get a meme!" +error: + embed: + title: "Failed Getting Meme" + description: "I... I'm so sorry... I tripped and fell when getting your meme..." +subcommand: + coffee: + description: "Get a coffee meme!" + random: + description: "Get a random meme!" + tea: + description: "Get a tea meme!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/menu.yml b/modules/i18n/src/main/resources/i18n/en/command/menu.yml new file mode 100644 index 000000000..fd11fd55f --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/menu.yml @@ -0,0 +1 @@ +description: "Hungry? Check the menu out!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/ping.yml b/modules/i18n/src/main/resources/i18n/en/command/ping.yml new file mode 100644 index 000000000..91d8370ed --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/ping.yml @@ -0,0 +1,10 @@ +description: "Pong! (US EDITION)" +embed: + description: "Hello!~ Would you like to order some coffee?" + shard: "Your shard ID is {SHARD_ID}! Keep note of this in case you need some help..." + author: "Author: beanbeanjuice - https://github.com/beanbeanjuice/cafeBot" +arguments: + word: + description: "Any word you want repeated back to you." + number: + description: "An integer to repeat back to you." diff --git a/modules/i18n/src/main/resources/i18n/en/command/rate.yml b/modules/i18n/src/main/resources/i18n/en/command/rate.yml new file mode 100644 index 000000000..01cfadee6 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/rate.yml @@ -0,0 +1,55 @@ +description: "Rate something!" +subcommand: + caffeinated: + description: "Rate how caffeinated you, or someone else, is!" + embed: + title: "☕ Caffeine Rating ☕" + description: "{user} is {percent}% caffeinated! " + arguments: + user: + description: "The person who's caffeine levels you want to see." + + gay: + description: "Rate how ✨ gay ✨ someone is!" + embed: + title: "🏳️‍🌈 Gay Rating 🌈" + description: "{user} is {percent}% gay! <:bloatedBlush:1154475611893547218>" + arguments: + user: + description: "The person who's gayness you want to see." + + insane: + description: "See how insane someone is!" + embed: + title: "‼️ Insane Rating ‼️" + description: "{user} is {percent}% insane! " + arguments: + user: + description: "The person who's insanity you want to see." + + poor: + description: "Rate how poor someone is!" + embed: + title: "💸 Poor Rating 💸️" + description: "{user} is {percent}% poor! 🤢" + arguments: + user: + description: "The person who's poor level you want to see." + + simp: + description: "Rate how much of a simp someone is!" + embed: + title: "😍 Simp Rating 😍" + description: "{user} is {percent}% simp! <:flushed_nervous:841923862202548224>" + arguments: + user: + description: "The person who's simp level you want to see." + + smart: + description: "Rate how smart someone is!" + embed: + title: "<:smartPeepo:1000248538376196280> Smart Rating <:smartPeepo:1000248538376196280>" + description: "{user} is {percent}% smart! 👩‍🎓" + arguments: + user: + description: "The person who's smart levels you want to see." diff --git a/modules/i18n/src/main/resources/i18n/en/command/roll.yml b/modules/i18n/src/main/resources/i18n/en/command/roll.yml new file mode 100644 index 000000000..23dfc52ff --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/roll.yml @@ -0,0 +1,5 @@ +description: "Roll dice!" +roll: "You rolled a **{number}**!" +arguments: + size: + description: "The size of the dice." diff --git a/modules/i18n/src/main/resources/i18n/en/command/serve.yml b/modules/i18n/src/main/resources/i18n/en/command/serve.yml new file mode 100644 index 000000000..3a8da9068 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/serve.yml @@ -0,0 +1,23 @@ +description: "Serve some words to customers to earn some Cafe Coins!" +embed: + success: + title: "Order up!" + description: "{user} has served {word} for **{reward}** CC! They now have **{balance}**!" + description_other: "{user} has served {word} to {customer} for **{reward}** CC! They now have **{balance}**!" + footer: "Type \"/serve\" to get some money!" + error: + word: + title: "Invalid Word" + description: "Umm... I don't think that's a real word... make sure you use a real, single, **non-banned** english word." + time: + title: "Cannot serve!" + description: | + You last served a word at . + + You need to wait an hour before you last served in order to serve again. + +arguments: + word: + description: "Any English word!" + customer: + description: "The user you want to serve the word to!" diff --git a/modules/i18n/src/main/resources/i18n/en/command/snipe.yml b/modules/i18n/src/main/resources/i18n/en/command/snipe.yml new file mode 100644 index 000000000..a72ac4491 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/snipe.yml @@ -0,0 +1,8 @@ +description: "Snipe a recently deleted message!" +embed: + none: + title: "No Snipe Found!" + description: "No one has deleted any messages in this channel recently..." + snipe: + title: "Snipe!" + description: "{user} said \"{message}\"" diff --git a/modules/i18n/src/main/resources/i18n/en/command/tictactoe.yml b/modules/i18n/src/main/resources/i18n/en/command/tictactoe.yml new file mode 100644 index 000000000..2b1c500ce --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/tictactoe.yml @@ -0,0 +1,13 @@ +description: "Play tic-tac-toe with someone!" +embeds: + bot: + title: "Cannot Play Against Bot" + description: "What do you think you're even doing? <:cafeBot_angry:1171726164092518441> I refuse to play against you." + consent: + title: "Waiting for Consent" + description: "{player1}, do you accept the challenge from {player2} for **{wager} cC** (cafeCoins)? Once accepted, games will be automatically closed after 24 hours." +arguments: + opponent: + description: "The person you want to play against." + wager: + description: "The amount you want to wager for this game." diff --git a/modules/i18n/src/main/resources/i18n/en/command/twitch.yml b/modules/i18n/src/main/resources/i18n/en/command/twitch.yml new file mode 100644 index 000000000..a55738fc1 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/command/twitch.yml @@ -0,0 +1,35 @@ +description: "Add or remove channels/notification channels." + +subcommands: + add: + description: "Add a twitch channel to get live notifications for!" + success: + title: "Added Channel" + description: "Successfully added **{channel}**." + error: + title: "Error Adding Channel" + description: "There was an error adding **{channel}**." + arguments: + username: + description: "Their twitch username." + + list: + description: "Get a list of all twitch channels for this server!" + success: + title: "Twitch Channels" + description: | + These are the following channels that have been added for this server: + + {channels} + + remove: + description: "Remove a twitch channel!" + success: + title: "Twitch Channel Removed" + description: "They were successfully removed. You should no longer receive live notifications from them." + error: + title: "Error Removing Twitch Channel" + description: "There was an error removing them, you may still receive live notifications for their channel." + arguments: + username: + description: "The username you want to remove." diff --git a/modules/i18n/src/main/resources/i18n/en/generic.yml b/modules/i18n/src/main/resources/i18n/en/generic.yml new file mode 100644 index 000000000..e5a3eff77 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/generic.yml @@ -0,0 +1,18 @@ +error: + uncaught: + title: "Uncaught Error!" + message: | + I-... something went... seriously wrong... <:cafeBot_sad:1171726165040447518> + + I tried to do that and short-circuited... this is the error I got... + + ``` + {uncaught_error} + ``` + permission: + title: "No Permission" + message: "What are you doing back here?? Get **out**!" + + generic: + title: "Error" + message: "There was an error! I... I'll try to let me boss know... please try again later and make a report on the GitHub if it is not fixed soon with `/bug`!" diff --git a/modules/i18n/src/main/resources/i18n/en/info.yml b/modules/i18n/src/main/resources/i18n/en/info.yml new file mode 100644 index 000000000..41c95e25b --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en/info.yml @@ -0,0 +1,4 @@ +bot: + name: "cafeBot" + greeting: "Hello, world!" + discord-locale: ENGLISH_US diff --git a/modules/i18n/src/main/resources/i18n/en_GB/command/ping.yml b/modules/i18n/src/main/resources/i18n/en_GB/command/ping.yml new file mode 100644 index 000000000..6aad3729a --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en_GB/command/ping.yml @@ -0,0 +1,10 @@ +description: "Pong! (UK EDITION)" +embed: + description: "Hello!~ Would you like to order some coffee?" + shard: "Your shard ID is {SHARD_ID}! Keep note of this in case you need some help..." + author: "Author: beanbeanjuice - https://github.com/beanbeanjuice/cafeBot" +arguments: + word: + description: "Any word you want repeated back to you." + number: + description: "An integer to repeat back to you." diff --git a/modules/i18n/src/main/resources/i18n/en_GB/info.yml b/modules/i18n/src/main/resources/i18n/en_GB/info.yml new file mode 100644 index 000000000..f3bf5a606 --- /dev/null +++ b/modules/i18n/src/main/resources/i18n/en_GB/info.yml @@ -0,0 +1,3 @@ +bot: + greeting: "Hello, eh?!" + discord-locale: ENGLISH_UK diff --git a/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/FolderPathTest.java b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/FolderPathTest.java new file mode 100644 index 000000000..43dd52408 --- /dev/null +++ b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/FolderPathTest.java @@ -0,0 +1,110 @@ +package com.beanbeanjuice.cafebot.i18n; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; +import java.util.Optional; + +public class FolderPathTest { + + @Test + @DisplayName("Test Single Folder") + public void testSingleFolder() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.ENGLISH); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "command.ai", Locale.ENGLISH); + + Assertions.assertTrue(result.isPresent()); + Assertions.assertEquals("i18n/en/command/ai.yml", result.get()); + } + + @Test + @DisplayName("Test No Folder") + public void testNoFolder() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.ENGLISH); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "generic", Locale.ENGLISH); + + Assertions.assertTrue(result.isPresent()); + Assertions.assertEquals("i18n/en/generic.yml", result.get()); + } + + @Test + @DisplayName("Test With Extra Key") + public void testWithExtraKey() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.ENGLISH); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "command.ai.description", Locale.ENGLISH); + + Assertions.assertTrue(result.isPresent()); + Assertions.assertEquals("i18n/en/command/ai.yml", result.get()); + } + + @Test + @DisplayName("Test File English Fallback") + public void testEnglishFallback() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.CHINA); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "command.ai.description", Locale.CHINA); + + Assertions.assertTrue(result.isPresent()); + Assertions.assertEquals("i18n/en/command/ai.yml", result.get()); + } + + @Test + @DisplayName("Test File with Valid Language") + public void testValidLanguage() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.UK); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "info.bot.greeting", Locale.UK); + + Assertions.assertTrue(result.isPresent()); + Assertions.assertEquals("i18n/en_GB/info.yml", result.get()); + } + + @Test + @DisplayName("Test Missing English File") + public void testMissingEnglishFile() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.ENGLISH); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "this.path.does.not.exist", Locale.ENGLISH); + + Assertions.assertFalse(result.isPresent()); + } + + @Test + @DisplayName("Test Missing Foreign and English Language File") + public void testMissingForeignLanguageFile() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.UK); + + Method method = I18N.class.getDeclaredMethod("findFilePath", String.class, Locale.class); + method.setAccessible(true); + + Optional result = (Optional) method.invoke(i18n, "this.path.does.not.exist", Locale.UK); + + Assertions.assertFalse(result.isPresent()); + } + +} diff --git a/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/I18NResourceVerificationTest.java b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/I18NResourceVerificationTest.java new file mode 100644 index 000000000..3b226caae --- /dev/null +++ b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/I18NResourceVerificationTest.java @@ -0,0 +1,145 @@ +package com.beanbeanjuice.cafebot.i18n; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class I18NResourceVerificationTest { + + private Set getLanguageFilePaths(String initialPath) throws URISyntaxException { + URL resource = I18NResourceVerificationTest.class.getClassLoader().getResource(initialPath); + if (resource == null) Assertions.fail("Resource not found: " + initialPath); + + URL i18nResources = I18NResourceVerificationTest.class.getClassLoader().getResource("i18n"); + if (i18nResources == null) Assertions.fail("Failed to find i18n resources"); + Path relativePath = Path.of(i18nResources.toURI()); + + try (Stream walk = Files.walk(Paths.get(resource.toURI()))) { + return walk.filter(Files::isRegularFile) + .map(Path::toString) + .map((string) -> string.replace(relativePath.toString(), "")) + .map((string) -> string.replaceFirst("\\\\", "")) + .collect(Collectors.toSet()); + } catch (IOException | URISyntaxException e) { + Assertions.fail(e); + } + + Assertions.fail("Resource not found: " + initialPath); + return null; + } + + @Test + @DisplayName("Verify Lowercase Paths") + public void verifyLowercasePaths() throws URISyntaxException { + // Verify that all file paths in all languages are lowercase. + Set allPaths = getLanguageFilePaths("i18n"); + Assertions.assertNotNull(allPaths); + + for (String path : allPaths) { + String[] splitPathOrig = path.split("\\\\"); + String[] splitPath = Arrays.copyOfRange(splitPathOrig, 1, splitPathOrig.length); + + for (String split : splitPath) { + Assertions.assertEquals(split.toLowerCase(), split); + } + } + } + + private List getAllKeysInMap(Map map) { + // If value at key is a map, go deeper. + List keys = new ArrayList<>(); + + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + keys.add(key); + if (value instanceof Map) keys.addAll(getAllKeysInMap((Map) value)); + } + + return keys; + } + + @Test + @DisplayName("Verify Lowercase Keys") + public void verifyLowercaseKeys() throws URISyntaxException { + // Verify that all key paths in all languages are lowercase. + Set paths = getLanguageFilePaths("i18n"); // en/info.yml + + for (String path : paths) { + String fullPath = "i18n\\" + path; + + URL resource = I18NResourceVerificationTest.class.getClassLoader().getResource(fullPath); + if (resource == null) Assertions.fail("Resource not found: " + fullPath); + + try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(fullPath)) { + Yaml yaml = new Yaml(); + Map map = yaml.load(is); + + List keys = getAllKeysInMap(map); + + for (String key : keys) Assertions.assertEquals(key, key.toLowerCase(), String.format("key '%s' is incorrect in %s", key, fullPath)); + } catch (IOException e) { + Assertions.fail(e); + } + } + } + + @Test + @DisplayName("Verify Valid YAML Files") + public void verifyValidYamlFiles() throws URISyntaxException { + Set paths = getLanguageFilePaths("i18n"); // en/info.yml + + for (String path : paths) { + String fullPath = "i18n\\" + path; // i18n/en/info.yml + + URL resource = I18NResourceVerificationTest.class.getClassLoader().getResource(fullPath); + if (resource == null) Assertions.fail("Resource not found: " + fullPath); + + try (InputStream is = this.getClass().getClassLoader().getResourceAsStream(fullPath)) { + + try { + Yaml yaml = new Yaml(); + Map map = yaml.load(is); + } catch (Exception e) { + Assertions.fail(e); + } + } catch (IOException e) { + Assertions.fail(e); + } + } + } + + @Test + @DisplayName("Verify Foreign Files are Subset of English Files") + @Disabled + public void testForeignFilesAreSubsetOfEnglishFiles() { + // Get a list of all file paths in the i18n/en directory. + Assertions.fail("Not yet implemented"); + } + + @Test + @DisplayName("Verify Foreign Keys are Subset of English Keys") + @Disabled + public void testForeignKeysAreSubsetOfEnglishKeys() { + Assertions.fail("Not yet implemented"); + } + +} diff --git a/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/I18NTest.java b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/I18NTest.java new file mode 100644 index 000000000..abdeb1e65 --- /dev/null +++ b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/I18NTest.java @@ -0,0 +1,97 @@ +package com.beanbeanjuice.cafebot.i18n; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Locale; + +public class I18NTest { + + @Test + @DisplayName("Can Get I18N from Folder") + public void canGetI18N() { + I18N i18n = new I18N(Locale.ENGLISH); + + String description = i18n.getString("command.ai.description"); + Assertions.assertEquals("Want a sassy AI that will serve you some coffee?", description); + } + + @Test + @DisplayName("Can Get I18N String Array") + public void canGetI18NStringArray() { + I18N i18n = new I18N(Locale.ENGLISH); + + List answers = i18n.getStringArray("command.eightball.answers.positive"); + + Assertions.assertFalse(answers.isEmpty()); + Assertions.assertEquals("It is likely...", answers.getFirst()); + } + + @Test + @DisplayName("Can Get I18N Fallback String Array") + public void canGetI18NFallbackStringArray() { + I18N i18n = new I18N(Locale.CHINA); + + List answers = i18n.getStringArray("command.eightball.answers.positive"); + + Assertions.assertFalse(answers.isEmpty()); + Assertions.assertEquals("It is likely...", answers.getFirst()); + } + + @Test + @DisplayName("Can Get I18N without Folder") + public void canGetI18NWithoutFolder() { + I18N i18n = new I18N(Locale.ENGLISH); + + String description = i18n.getString("info.bot.name"); + Assertions.assertEquals("cafeBot", description); + } + + @Test + @DisplayName("Test With Invalid Key") + public void testWithInvalidKey() { + I18N i18n = new I18N(Locale.ENGLISH); + + String description = i18n.getString("this.key.does.not.exist"); + Assertions.assertEquals("this.key.does.not.exist", description); + } + + @Test + @DisplayName("Test Foreign Language") + public void testForeignLanguage() { + I18N i18n = new I18N(Locale.UK); + + String description = i18n.getString("info.bot.greeting"); + Assertions.assertEquals("Hello, eh?!", description); + } + + @Test + @DisplayName("Test Valid English Fallback - Missing Foreign, Valid English") + public void testValidEnglishFallback() { + I18N i18n = new I18N(Locale.CHINA); + + String description = i18n.getString("info.bot.name"); + Assertions.assertEquals("cafeBot", description); + } + + @Test + @DisplayName("Test Invalid English Fallback - Missing Foreign, Missing English") + public void testInvalidEnglishFallback() { + I18N i18n = new I18N(Locale.CHINA); + + String description = i18n.getString("this.key.does.not.exist"); + Assertions.assertEquals("this.key.does.not.exist", description); + } + + @Test + @DisplayName("Test Valid English Fallback - Valid Foreign, Missing Key, Valid English") + public void testValidEnglishFallbackWithKey() { + I18N i18n = new I18N(Locale.UK); + + String description = i18n.getString("info.bot.name"); + Assertions.assertEquals("cafeBot", description); + } + +} diff --git a/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/ParseKeyTest.java b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/ParseKeyTest.java new file mode 100644 index 000000000..6bef061ef --- /dev/null +++ b/modules/i18n/src/test/java/com/beanbeanjuice/cafebot/i18n/ParseKeyTest.java @@ -0,0 +1,39 @@ +package com.beanbeanjuice.cafebot.i18n; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Locale; + +public class ParseKeyTest { + + @Test + @DisplayName("Test Valid Key Parse") + public void testValidKeyParse() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.US); + + Method method = I18N.class.getDeclaredMethod("parseKey", String.class, String.class); + method.setAccessible(true); + + String result = (String) method.invoke(i18n, "i18n/en/command/ai.yml", "command.ai.embed.title"); + + Assertions.assertEquals("embed.title", result); + } + + @Test + @DisplayName("Test Invalid Key Parse") + public void testInvalidKeyParse() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + I18N i18n = new I18N(Locale.US); + + Method method = I18N.class.getDeclaredMethod("parseKey", String.class, String.class); + method.setAccessible(true); + + String result = (String) method.invoke(i18n, "i18n/en/command/ai.yml", "this.is.not.a.real.command"); + + Assertions.assertEquals("this.is.not.a.real.command", result); + } + +} diff --git a/modules/i18n/src/test/resources/junit-platform.properties b/modules/i18n/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..ff11acf87 --- /dev/null +++ b/modules/i18n/src/test/resources/junit-platform.properties @@ -0,0 +1,8 @@ +# Enable parallel execution +junit.jupiter.execution.parallel.enabled = true + +# Classes can run in parallel +junit.jupiter.execution.parallel.mode.classes.default = concurrent + +# Methods inside a class run sequentially +junit.jupiter.execution.parallel.mode.default = same_thread diff --git a/modules/meme-api-wrapper/src/main/java/com/beanbeanjuice/meme/api/wrapper/MemeAPI.java b/modules/meme-api-wrapper/src/main/java/com/beanbeanjuice/meme/api/wrapper/MemeAPI.java index 95083f605..6ecf62370 100644 --- a/modules/meme-api-wrapper/src/main/java/com/beanbeanjuice/meme/api/wrapper/MemeAPI.java +++ b/modules/meme-api-wrapper/src/main/java/com/beanbeanjuice/meme/api/wrapper/MemeAPI.java @@ -7,40 +7,32 @@ import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; import org.apache.hc.client5.http.impl.async.HttpAsyncClients; import org.apache.hc.core5.concurrent.FutureCallback; -import org.apache.hc.core5.http.HttpHost; -import org.apache.hc.core5.http.Method; import tools.jackson.databind.JsonNode; import tools.jackson.databind.json.JsonMapper; -import java.net.URISyntaxException; import java.util.concurrent.CompletableFuture; public class MemeAPI { private MemeAPI() { } // Should not be able to instantiate. - public static CompletableFuture get(String subreddit) { - HttpHost host; - - try { - host = HttpHost.create("https://meme-api.com"); - } catch (URISyntaxException e) { - return CompletableFuture.failedFuture(e); - } + // Only one reusable client should exist. Good practice for ASYNC clients. + private static final CloseableHttpAsyncClient CLIENT = HttpAsyncClients.createDefault(); - SimpleHttpRequest request = SimpleRequestBuilder - .create(Method.GET) - .setHttpHost(host) - .setPath(String.format("/gimme/%s", subreddit)) - .build(); + static { + CLIENT.start(); + } - CloseableHttpAsyncClient client = HttpAsyncClients.custom().build(); - client.start(); + private static final JsonMapper MAPPER = new JsonMapper(); - JsonMapper mapper = new JsonMapper(); + public static CompletableFuture get(String subreddit) { CompletableFuture future = new CompletableFuture<>(); - client.execute(request, new FutureCallback<>() { + SimpleHttpRequest request = SimpleRequestBuilder + .get("https://meme-api.com/gimme/" + subreddit) + .build(); + + CLIENT.execute(request, new FutureCallback<>() { @Override public void completed(SimpleHttpResponse response) { @@ -48,21 +40,15 @@ public void completed(SimpleHttpResponse response) { int statusCode = response.getCode(); String body = response.getBodyText(); - JsonNode jsonNode; - - if (statusCode == 204 || body == null || body.isBlank()) { - // No content → represent as an empty object or null - jsonNode = mapper.createObjectNode(); // or null, depending on your design - } else { - jsonNode = mapper.readTree(body); - } - if (statusCode < 200 || statusCode >= 300) { future.completeExceptionally( - new IllegalStateException(String.format("Request failed with status %d.", statusCode)) + new IllegalStateException("Request failed: " + statusCode) ); + return; } + JsonNode jsonNode = MAPPER.readTree(body); + RedditMeme meme = new RedditMeme( jsonNode.get("title").asString(), jsonNode.get("subreddit").asString(), @@ -73,36 +59,24 @@ public void completed(SimpleHttpResponse response) { ); future.complete(meme); + } catch (Exception e) { future.completeExceptionally(e); - } finally { - closeClient(client); } } @Override public void failed(Exception ex) { future.completeExceptionally(ex); - closeClient(client); } @Override public void cancelled() { future.cancel(true); - closeClient(client); } - }); return future; } - private static void closeClient(CloseableHttpAsyncClient client) { - try { - client.close(); - } catch (Exception e) { - // Log or ignore - } - } - } diff --git a/settings.gradle.kts b/settings.gradle.kts index a96369303..baaa9ad87 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,8 +2,10 @@ rootProject.name = "cafeBot" include( "modules:cafeBot-api-wrapper", - "modules:meme-api-wrapper" + "modules:meme-api-wrapper", + "modules:i18n" ) project(":modules:cafeBot-api-wrapper").name = "cafeBot-api-wrapper" project(":modules:meme-api-wrapper").name = "meme-api-wrapper" +project(":modules:i18n").name = "i18n" diff --git a/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java b/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java index e1119d06b..de208d3ec 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java +++ b/src/main/java/com/beanbeanjuice/cafebot/CafeBot.java @@ -91,9 +91,18 @@ public class CafeBot { @Getter private SnipeHandler snipeHandler; // Additional Items - @Getter private final AtomicInteger commandsRun = new AtomicInteger(0); // Atomic since can run across multiple shards. + @Getter private static final AtomicInteger commandsRun = new AtomicInteger(0); // Atomic since can run across multiple shards. @Getter private final String discordAvatarUrl = "https://cdn.beanbeanjuice.com/images/cafeBot/cafeBot.gif"; + private void checkApiConnection() { + try { + Greeting greeting = cafeAPI.getGreetingApi().getAdminHello().get(); + if (greeting == null || greeting.getMessage() == null) throw new Exception("Greeting not found!"); + } catch (Exception e) { + this.logger.log(CafeBot.class, LogLevel.ERROR, "Connection to the API is invalid...", true, true); + } + } + public CafeBot() throws InterruptedException, ExecutionException { this.logger = new LogManager( this, @@ -110,10 +119,7 @@ public CafeBot() throws InterruptedException, ExecutionException { this.logger.addWebhookURL(EnvironmentVariable.CAFEBOT_GUILD_WEBHOOK_URL.getSystemVariable()); this.logger.log(CafeBot.class, LogLevel.OKAY, "Starting bot!", true, false); - Greeting greeting = cafeAPI.getGreetingApi().getAdminHello().get(); - if (greeting == null || greeting.getMessage() == null) { - this.logger.log(CafeBot.class, LogLevel.ERROR, "Connection to the API is invalid...", true, true); - } + this.checkApiConnection(); this.shardManager = DefaultShardManagerBuilder .createDefault(EnvironmentVariable.CAFEBOT_TOKEN.getSystemVariable()) @@ -352,8 +358,8 @@ public void pmUser(User user, MessageEmbed embed) { user.openPrivateChannel().flatMap(channel -> channel.sendMessageEmbeds(embed)).queue(); } - public void increaseCommandsRun() { - this.commandsRun.incrementAndGet(); + public static void increaseCommandsRun() { + commandsRun.incrementAndGet(); } public SelfUser getSelfUser() { diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/BalanceCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/BalanceCommand.java index cc96a19af..ffe6a2c7b 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/BalanceCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/BalanceCommand.java @@ -3,8 +3,10 @@ import com.beanbeanjuice.cafebot.api.wrapper.type.DiscordUser; import com.beanbeanjuice.cafebot.api.wrapper.type.MenuOrder; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.EmbedBuilder; @@ -18,6 +20,7 @@ import java.util.Arrays; import java.util.Optional; +import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; public class BalanceCommand extends Command implements ICommand { @@ -27,35 +30,39 @@ public BalanceCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); CompletableFuture f1 = bot.getCafeAPI().getUserApi().getUser(user.getId()); CompletableFuture f2 = bot.getCafeAPI().getOrderApi().getOrders(user.getId()); + I18N bundle = ctx.getUserI18n(); + f1.thenAcceptBoth(f2, (discordUser, orders) -> { - event.getHook().sendMessageEmbeds(balanceEmbed(user, discordUser, orders)).queue(); + event.getHook().sendMessageEmbeds(balanceEmbed(user, discordUser, orders, bundle)).queue(); }).exceptionally((e) -> { event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Error Getting Balance", - "There was some sort of error getting your coin balance." + bundle.getString("command.balance.error.title"), + bundle.getString("command.balance.error.description") )).queue(); return null; }); } - public MessageEmbed balanceEmbed(User user, DiscordUser cafeUser, MenuOrder[] orders) { + public MessageEmbed balanceEmbed(User user, DiscordUser cafeUser, MenuOrder[] orders, I18N bundle) { long ordersBought = Arrays.stream(orders).filter((order) -> order.getFromId().equals(user.getId())).count(); long ordersReceived = Arrays.stream(orders).filter((order) -> order.getToId().equals(user.getId())).count(); + String description = bundle.getString("command.balance.embed.description").replace("{user}", user.getAsMention()).replace("{balance}", String.valueOf(Helper.roundFloat(cafeUser.getBalance()))); + return new EmbedBuilder() - .setTitle("cafeCoin Balance") + .setTitle(bundle.getString("command.balance.embed.title")) .setColor(Helper.getRandomColor()) - .addField("Orders Bought", String.valueOf(ordersBought), true) - .addField("Orders Received", String.valueOf(ordersReceived), true) - .setDescription(user.getAsMention() + " has a current balance of `$" + Helper.roundFloat(cafeUser.getBalance()) + "` cC (cafeCoins)!") - .setFooter("To learn how to make money do /help") + .addField(bundle.getString("command.balance.embed.orders.bought"), String.valueOf(ordersBought), true) + .addField(bundle.getString("command.balance.embed.orders.received"), String.valueOf(ordersReceived), true) + .setDescription(description) + .setFooter(bundle.getString("command.balance.embed.footer")) .build(); } @@ -65,8 +72,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get your balance!"; + public String getDescriptionPath() { + return "command.balance.description"; } @Override @@ -77,7 +84,7 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The user you want to get the balance of.", false) + new OptionData(OptionType.USER, "user", "command.balance.arguments.user.description", false) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/DonateCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/DonateCommand.java index 7d4c514e3..fe42dfc44 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/DonateCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/DonateCommand.java @@ -2,15 +2,19 @@ import com.beanbeanjuice.cafebot.api.wrapper.api.exception.ApiRequestException; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.logging.LogLevel; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.DiscordLocale; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; @@ -18,6 +22,7 @@ import java.time.Instant; import java.util.Optional; +import java.util.concurrent.CompletionException; public class DonateCommand extends Command implements ICommand { @@ -26,13 +31,16 @@ public DonateCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional receiverMapping = Optional.ofNullable(event.getOption("user")); User receiver = receiverMapping.map(OptionMapping::getAsUser).orElseThrow(); // Shouldn't be null. User sender = event.getUser(); if (receiver == sender) { - event.getHook().sendMessageEmbeds(Helper.errorEmbed("Self Donation", "You can't donate to yourself!")).queue(); + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + ctx.getUserI18n().getString("command.donate.embed.self.title"), + ctx.getUserI18n().getString("command.donate.embed.self.description") + )).queue(); return; } @@ -42,42 +50,47 @@ public void handle(SlashCommandInteractionEvent event) { bot.getCafeAPI().getDonationApi().createDonation(sender.getId(), receiver.getId(), amount) .thenAccept((result) -> { sendSuccessToSender(event, receiver); - sendDonationEmbed(event, sender, receiver, amount); + sendDonationEmbed(event, sender, receiver, amount, ctx.getGuildI18n()); }) .exceptionally((e) -> { - if (e.getCause() instanceof ApiRequestException requestException) { - JsonNode error = requestException.getBody(); - - if (error.get("from") != null && error.get("from").get(0).asString().equals("Insufficient balance")) { - sendNotEnoughCoinsEmbed(event, sender.getId(), amount); - return null; - } - - if (error.get("to") != null && error.get("to").get(0).asString().equals("That user can only be donated to once per hour")) { - sendNeedToWaitEmbed(event, receiver.getId()); - return null; - } - } - - event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Error Donating", - "There was an error donating to the user: " + e.getMessage() - )).queue(); - - bot.getLogger().log(DonateCommand.class, LogLevel.WARN, "There was an error donating to a user: " + e.getMessage(), e.getCause()); - return null; + handleError(e, event, ctx, sender, receiver, amount); + + throw new CompletionException(e.getCause()); }); } - private void sendNotEnoughCoinsEmbed(SlashCommandInteractionEvent event, String userId, double amount) { + private void handleError(Throwable e, SlashCommandInteractionEvent event, CommandContext ctx, User sender, User receiver, double amount) { + if (e.getCause() instanceof ApiRequestException requestException) { + JsonNode error = requestException.getBody().get("error"); + + if (error.get("from") != null && error.get("from").get(0).asString().equals("Insufficient balance")) { + sendNotEnoughCoinsEmbed(event, sender.getId(), amount, ctx.getUserI18n()); + return; + } + + if (error.get("to") != null && error.get("to").get(0).asString().equals("That user can only be donated to once per hour")) { + sendNeedToWaitEmbed(event, receiver.getId(), ctx.getUserI18n()); + return; + } + } + + event.getHook().sendMessageEmbeds(Helper.uncaughtErrorEmbed( + ctx.getUserI18n(), + e.getMessage() + )).queue(); + + bot.getLogger().log(DonateCommand.class, LogLevel.WARN, "There was an error donating to a user: " + e.getMessage(), e.getCause()); + } + + private void sendNotEnoughCoinsEmbed(SlashCommandInteractionEvent event, String userId, double amount, I18N i18n) { bot.getCafeAPI().getUserApi().getUser(userId).thenAccept((user) -> { - event.getHook().sendMessageEmbeds(notEnoughCoinsEmbed(user.getBalance(), amount)).queue(); + event.getHook().sendMessageEmbeds(notEnoughCoinsEmbed(user.getBalance(), amount, i18n)).queue(); }); } - private void sendNeedToWaitEmbed(SlashCommandInteractionEvent event, String userId) { + private void sendNeedToWaitEmbed(SlashCommandInteractionEvent event, String userId, I18N i18n) { bot.getCafeAPI().getUserApi().getUser(userId).thenAccept((user) -> { - event.getHook().sendMessageEmbeds(needToWaitEmbed(user.getLastDonationTime().orElse(Instant.now()))).queue(); + event.getHook().sendMessageEmbeds(needToWaitEmbed(user.getLastDonationTime().orElse(Instant.now()), i18n)).queue(); }); } @@ -85,29 +98,35 @@ private void sendSuccessToSender(SlashCommandInteractionEvent event, User receiv event.getHook().sendMessageEmbeds(successEmbed(receiver)).queue(); } - private void sendDonationEmbed(SlashCommandInteractionEvent event, User sender, User receiver, double amount) { - event.getChannel().sendMessageEmbeds(donationEmbed(sender, receiver, amount)).mention(receiver).queue(); + private void sendDonationEmbed(SlashCommandInteractionEvent event, User sender, User receiver, double amount, I18N i18n) { + event.getChannel().sendMessageEmbeds(donationEmbed(sender, receiver, amount, i18n)).mention(receiver).queue(); } - private MessageEmbed notEnoughCoinsEmbed(double balance, double amount) { - return Helper.errorEmbed( - "Not Enough Coins", - String.format(""" - Sorry, you don't have enough bC to donate to that person. \ - You have **%.2f bC**, which is *not* enough to donate **%.2f bC** to that person. \ - Maybe try working for a bit? You can use `/serve` to work. - """, balance, amount) - ); + private MessageEmbed notEnoughCoinsEmbed(double balance, double amount, I18N i18n) { + String description = i18n.getString("command.donate.embed.balance.description") + .replace("{balance}", String.format("%.2f", balance)) + .replace("{payment}", String.format("%.2f", amount)); + + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setTitle(i18n.getString("command.donate.embed.balance.title")); + embedBuilder.setDescription(description); + embedBuilder.setFooter(i18n.getString("command.donate.embed.balance.footer")); + embedBuilder.setColor(Helper.getRandomColor()); + + return embedBuilder.build(); } - private MessageEmbed needToWaitEmbed(Instant lastDonationTime) { - return Helper.errorEmbed( - "That User Can't Receive Donations", - String.format(""" - That user was last donated to at . You need to wait before they are able to \ - receive another donation. - """, lastDonationTime.getEpochSecond()) - ); + private MessageEmbed needToWaitEmbed(Instant lastDonationTime, I18N i18n) { + String description = i18n.getString("command.donate.embed.cooldown.description") + .replace("{timestamp}", String.valueOf(lastDonationTime.getEpochSecond())); + + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setTitle(i18n.getString("command.donate.embed.cooldown.title")); + embedBuilder.setDescription(description); + embedBuilder.setFooter(i18n.getString("command.donate.embed.cooldown.footer")); + embedBuilder.setColor(Helper.getRandomColor()); + + return embedBuilder.build(); } private MessageEmbed successEmbed(User receiver) { @@ -117,15 +136,19 @@ private MessageEmbed successEmbed(User receiver) { ); } - private MessageEmbed donationEmbed(User sender, User receiver, double amount) { - return Helper.successEmbed( - "Donation!", - String.format(""" - Aww!~ - - %s just donated **%.2f bC** to %s! - """, sender.getAsMention(), amount, receiver.getAsMention()) - ); + private MessageEmbed donationEmbed(User sender, User receiver, double amount, I18N i18n) { + String description = i18n.getString("command.donate.embed.donation.description") + .replace("{donator}", sender.getAsMention()) + .replace("{donatee}", receiver.getAsMention()) + .replace("{amount}", String.format("%.2f", amount)); + + EmbedBuilder embedBuilder = new EmbedBuilder(); + embedBuilder.setTitle(i18n.getString("command.donate.embed.donation.title")); + embedBuilder.setDescription(description); + embedBuilder.setFooter(i18n.getString("command.donate.embed.donation.footer")); + embedBuilder.setColor(Helper.getRandomColor()); + + return embedBuilder.build(); } @Override @@ -134,8 +157,8 @@ public String getName() { } @Override - public String getDescription() { - return "Donate your beanCoins to another user!"; + public String getDescriptionPath() { + return "command.donate.description"; } @Override @@ -146,8 +169,8 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The user you want to donate to.", true), - new OptionData(OptionType.NUMBER, "amount", "The amount of beanCoins you want to donate.", true) + new OptionData(OptionType.USER, "user", "command.donate.arguments.user.description", true), + new OptionData(OptionType.NUMBER, "amount", "command.donate.arguments.amount.description", true) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/MenuCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/MenuCommand.java index 7c9f0059f..6693cb21b 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/MenuCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/MenuCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.components.actionrow.ActionRow; @@ -15,7 +16,7 @@ public MenuCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { event.getHook() .sendMessageEmbeds(bot.getMenuHandler().getAllMenuEmbed()) .addComponents(ActionRow.of(bot.getMenuHandler().getAllStringSelectMenu())) @@ -28,8 +29,8 @@ public String getName() { } @Override - public String getDescription() { - return "Hungry? Check the menu out!"; + public String getDescriptionPath() { + return "command.menu.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/ServeCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/ServeCommand.java index df339c5ef..8d10b6624 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/ServeCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/cafe/ServeCommand.java @@ -2,11 +2,14 @@ import com.beanbeanjuice.cafebot.api.wrapper.api.exception.ApiRequestException; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.logging.LogLevel; +import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; @@ -14,10 +17,12 @@ import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import org.jetbrains.annotations.Nullable; import tools.jackson.databind.JsonNode; import java.time.Instant; import java.util.Optional; +import java.util.concurrent.CompletionException; public class ServeCommand extends Command implements ICommand { @@ -26,7 +31,7 @@ public ServeCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { String word = event.getOption("word").getAsString(); // ! - Shouldn't be null. User user = event.getUser(); Optional optionalReceiver = Optional.ofNullable(event.getOption("customer")); @@ -35,66 +40,73 @@ public void handle(SlashCommandInteractionEvent event) { float reward = cafeUser.getReward(); float newBalance = cafeUser.getNewBalance(); - MessageEmbed embed = optionalReceiver - .map(OptionMapping::getAsUser) - .map((receiver) -> serveOtherEmbed(user, receiver, word, reward, newBalance)) - .orElse(serveSelfEmbed(user, word, reward, newBalance)); + User receiver = optionalReceiver.map(OptionMapping::getAsUser).orElse(null); + + MessageEmbed embed = serveEmbed(user, receiver, word, reward, newBalance, ctx.getGuildI18n()); event.getHook().sendMessageEmbeds(embed).queue(); }).exceptionallyAsync((e) -> { - if (e.getCause() instanceof ApiRequestException apiRequestException) { - JsonNode body = apiRequestException.getBody(); - - if (body.has("lastServeTime")) { - Instant lastServeTime = Instant.parse(body.get("lastServeTime").asString()); - event.getHook().sendMessageEmbeds(cannotServeEmbed(lastServeTime)).queue(); - return null; - } - - if (body.has("error") && body.get("error").has("word")) { - event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "There was an error serving!", - String.format("Please make sure to use a real, single, **non-banned** english word: %s", body.get("error").get("word").get(0).asString()) - )).queue(); - return null; - } - return null; - } + handleError(e, event, ctx); + throw new CompletionException(e.getCause()); + }); - event.getHook().sendMessageEmbeds(Helper.defaultErrorEmbed()).queue(); - bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Serving Word: " + e.getMessage(), true, true); + } - return null; - }); + private void handleError(Throwable e, SlashCommandInteractionEvent event, CommandContext ctx) { + if (e.getCause() instanceof ApiRequestException apiRequestException) { + JsonNode body = apiRequestException.getBody(); + + if (body.has("lastServeTime")) { + Instant lastServeTime = Instant.parse(body.get("lastServeTime").asString()); + event.getHook().sendMessageEmbeds(cannotServeEmbed(lastServeTime, ctx.getUserI18n())).queue(); + return; + } + + if (body.has("error") && body.get("error").has("word")) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + ctx.getUserI18n().getString("command.serve.embed.error.word.title"), + ctx.getUserI18n().getString("command.serve.embed.error.word.description") + )).queue(); + return; + } + } + event.getHook().sendMessageEmbeds(Helper.uncaughtErrorEmbed(ctx.getUserI18n(), e.getMessage())).queue(); + bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Serving Word: " + e.getMessage(), true, true); } - private MessageEmbed cannotServeEmbed(Instant lastServeTime) { + private MessageEmbed cannotServeEmbed(Instant lastServeTime, I18N i18n) { + String description = i18n.getString("command.serve.embed.error.time.description") + .replace("{time}", String.valueOf(lastServeTime.getEpochSecond())); + return Helper.errorEmbed( - "Cannot Serve", - String.format(""" - You served . - You need to wait an hour before you last served in order to serve again... - """, lastServeTime.getEpochSecond()) + i18n.getString("command.serve.embed.error.time.title"), + description ); } - private MessageEmbed serveSelfEmbed(User user, String word, float reward, float newBalance) { - return Helper.successEmbed( - "Order Up!", - String.format(""" - %s has served %s for **%.2f bC**! They now have **%.2f cC**! - """, user.getAsMention(), word, reward, newBalance) - ); - } + private MessageEmbed serveEmbed(User user, @Nullable User receiver, String word, float reward, float newBalance, I18N i18n) { + EmbedBuilder embedBuilder = new EmbedBuilder(); - private MessageEmbed serveOtherEmbed(User sender, User receiver, String word, double reward, float newBalance) { - return Helper.successEmbed( - "Order Up!", - String.format(""" - %s has served %s to %s for **%.2f cC**! They now have **%.2f cC**! - """, sender.getAsMention(), receiver.getAsMention(), word, reward, newBalance) - ); + embedBuilder.setTitle(i18n.getString("command.serve.embed.success.title")); + + String description = i18n.getString("command.serve.embed.success.description"); + if (receiver != null) { + description = i18n.getString("command.serve.embed.success.description_other") + .replace("{customer}", receiver.getAsMention()); + } + + description = description + .replace("{user}", user.getAsMention()) + .replace("{word}", word) + .replace("{balance}", String.valueOf(newBalance)) + .replace("{reward}", Float.toString(reward)); + + embedBuilder.setDescription(description); + embedBuilder.setFooter(i18n.getString("command.serve.embed.success.footer")); + embedBuilder.setColor(Helper.getRandomColor()); + + return embedBuilder.build(); } @Override @@ -103,8 +115,8 @@ public String getName() { } @Override - public String getDescription() { - return "Serve some words to customers to earn some bC (beanCoins)!"; + public String getDescriptionPath() { + return "command.serve.description"; } @Override @@ -115,8 +127,8 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.STRING, "word", "Any english word!", true), - new OptionData(OptionType.MENTIONABLE, "customer", "The customer you want to serve the word to.", false) + new OptionData(OptionType.STRING, "word", "command.serve.arguments.word.description", true), + new OptionData(OptionType.MENTIONABLE, "customer", "command.serve.arguments.customer.description", false) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AiCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AiCommand.java index bfa0806cd..e66a7e692 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AiCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AiCommand.java @@ -4,6 +4,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.logging.LogLevel; @@ -19,22 +20,28 @@ public AiCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { String guildId = event.getGuild().getId(); boolean status = event.getOption("status").getAsBoolean(); - String statusString = (status) ? "Enabled ✅" : "Disabled ❌"; - // TODO: Update syntax for this later bot.getCafeAPI().getGuildApi().updateDiscordServer(guildId, new DiscordServer(null, 0, status)).thenRun(() -> { + String title = ctx.getUserI18n().getString("command.ai.embed.title"); + String enabledString = ctx.getUserI18n().getString("command.ai.embed.enabled"); + String disabledString = ctx.getUserI18n().getString("command.ai.embed.disabled"); + String statusString = (status) ? enabledString : disabledString; + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "AI Response Changed", - String.format("Status: %s", statusString) + title, + statusString )).queue(); }).exceptionally((ex) -> { + String title = ctx.getUserI18n().getString("command.ai.error.title"); + String description = ctx.getUserI18n().getString("command.ai.error.description"); + event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Error Changing AI Response", - "There was an error changing the AI response... I'm so sorry..." + title, + description )).queue(); bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Updating AI Response: " + ex.getMessage(), true, false); @@ -48,8 +55,8 @@ public String getName() { } @Override - public String getDescription() { - return "Want a sassy AI that will serve you some coffee?"; + public String getDescriptionPath() { + return "command.ai.description"; } @Override @@ -60,7 +67,7 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.BOOLEAN, "status", "Whether to enable or disable the \"AI\".", true) + new OptionData(OptionType.BOOLEAN, "status", "command.ai.arguments.status.description", true) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AvatarCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AvatarCommand.java index 05669eed2..e4e75e8e3 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AvatarCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/AvatarCommand.java @@ -1,8 +1,10 @@ package com.beanbeanjuice.cafebot.commands.fun; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.EmbedBuilder; @@ -24,57 +26,59 @@ public AvatarCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userOption = Optional.ofNullable(event.getOption("user")); Optional serverOption = Optional.ofNullable(event.getOption("server")); User user = userOption.map(OptionMapping::getAsUser).orElse(event.getUser()); - Member member = userOption.map(OptionMapping::getAsMember).orElse(event.getMember()); + Member member = userOption.map(OptionMapping::getAsMember).orElseGet(event::getMember); boolean useServer = serverOption.map(OptionMapping::getAsBoolean).orElse(false); if (useServer && !event.isFromGuild()) { - event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Must Be In Server", - "You must be in a Discord server to use the server avatar!" - )).queue(); + event.getHook().sendMessageEmbeds(serverErrorEmbed(ctx.getUserI18n())).queue(); return; } - Optional urlOptional = (useServer) ? getServerAvatarURL(member) : getUserAvatarURL(user); + Optional urlOptional = Optional.ofNullable(member) + .filter(m -> useServer) + .map(Member::getAvatarUrl) + .or(() -> Optional.ofNullable(user.getAvatarUrl())); urlOptional.ifPresentOrElse( - (url) -> event.getHook().sendMessageEmbeds(avatarEmbed(user.getName(), url)).queue(), - () -> event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "No User Avatar", - "The specified user does not have a Discord avatar." - )).queue() + (url) -> event.getHook().sendMessageEmbeds(avatarEmbed(user.getName(), url, ctx.getUserI18n())).queue(), + () -> event.getHook().sendMessageEmbeds(missingErrorEmbed(ctx.getUserI18n())).queue() ); } - private Optional getUserAvatarURL(final User user) { - return Optional.ofNullable(user.getAvatarUrl()); - } - - private Optional getServerAvatarURL(final Member member) { - return Optional.ofNullable(member.getAvatarUrl()); - } - - private MessageEmbed avatarEmbed(final String name, final String avatarURL) { + private MessageEmbed avatarEmbed(String name, String avatarURL, I18N i18n) { + String title = i18n.getString("command.avatar.embed.title").replace("{user}", name); return new EmbedBuilder() - .setTitle(name + "'s Avatar") + .setTitle(title) .setImage(avatarURL + "?size=512") .setColor(Helper.getRandomColor()) .build(); } + private MessageEmbed missingErrorEmbed(I18N i18n) { + String title = i18n.getString("command.avatar.error.missing.title"); + String description = i18n.getString("command.avatar.error.missing.description"); + return Helper.errorEmbed(title, description); + } + + private MessageEmbed serverErrorEmbed(I18N i18n) { + String title = i18n.getString("command.avatar.error.server.title"); + String description = i18n.getString("command.avatar.error.server.description"); + return Helper.errorEmbed(title, description); + } + @Override public String getName() { return "avatar"; } @Override - public String getDescription() { - return "Get someone's avatar!"; + public String getDescriptionPath() { + return "command.avatar.description"; } @Override @@ -85,8 +89,8 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The user you want to get the avatar of.", false), - new OptionData(OptionType.BOOLEAN, "server", "Whether to get their server or user avatar.", false) + new OptionData(OptionType.USER, "user", "command.avatar.arguments.user.description", false), + new OptionData(OptionType.BOOLEAN, "server", "command.avatar.arguments.server.description", false) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/BannerCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/BannerCommand.java index 29e1a44e6..f51d555b4 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/BannerCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/BannerCommand.java @@ -1,8 +1,10 @@ package com.beanbeanjuice.cafebot.commands.fun; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.EmbedBuilder; @@ -24,7 +26,7 @@ public BannerCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userOption = Optional.ofNullable(event.getOption("user")); User user = userOption.map(OptionMapping::getAsUser).orElse(event.getUser()); @@ -33,33 +35,39 @@ public void handle(SlashCommandInteractionEvent event) { Optional urlOptional = Optional.ofNullable(profile.getBannerUrl()); urlOptional.ifPresentOrElse( - (url) -> event.getHook().sendMessageEmbeds(bannerEmbed(user.getName(), user.getAvatarUrl(), url, profile.getAccentColor())).queue(), - () -> event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "No User Banner", - "The specified user does not have a Discord banner." - )).queue() + (url) -> event.getHook().sendMessageEmbeds(bannerEmbed(user.getName(), user.getAvatarUrl(), url, profile.getAccentColor(), ctx.getUserI18n())).queue(), + () -> event.getHook().sendMessageEmbeds(noBannerError(ctx.getUserI18n())).queue() ); }); } - private MessageEmbed bannerEmbed(final String username, final String avatarURL, final String bannerURL, final Color accent) { + private MessageEmbed bannerEmbed(String username, String avatarURL, String bannerURL, Color accent, I18N i18n) { + String title = i18n.getString("command.banner.embed.title").replace("{user}", username); + EmbedBuilder embedBuilder = new EmbedBuilder() .setColor(accent) - .setAuthor(username + "'s Banner", null, avatarURL); + .setAuthor(title, null, avatarURL); embedBuilder.setImage(bannerURL + "?size=600"); return embedBuilder.build(); } + private MessageEmbed noBannerError(I18N i18n) { + String title = i18n.getString("command.banner.error.title"); + String description = i18n.getString("command.banner.error.description"); + + return Helper.errorEmbed(title, description); + } + @Override public String getName() { return "banner"; } @Override - public String getDescription() { - return "Get someone's banner!"; + public String getDescriptionPath() { + return "command.banner.description"; } @Override @@ -70,7 +78,7 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person you want to get the banner of.", false), + new OptionData(OptionType.USER, "user", "command.banner.arguments.user.description", false), }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/EightBallCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/EightBallCommand.java index b0be18023..be6467aa6 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/EightBallCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/EightBallCommand.java @@ -1,8 +1,10 @@ package com.beanbeanjuice.cafebot.commands.fun; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.EmbedBuilder; @@ -12,6 +14,8 @@ import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import java.util.List; + public class EightBallCommand extends Command implements ICommand { public EightBallCommand(final CafeBot cafeBot) { @@ -19,42 +23,31 @@ public EightBallCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { String question = event.getOption("question").getAsString(); - event.getHook().sendMessageEmbeds(getAnswerEmbed(question, getAnswer())).queue(); + String answer = getAnswer(ctx.getDefaultBundle()); + MessageEmbed answerEmbed = getAnswerEmbed(question, answer, ctx); + event.getHook().sendMessageEmbeds(answerEmbed).queue(); } - private MessageEmbed getAnswerEmbed(final String question, final String answer) { + private MessageEmbed getAnswerEmbed(String question, String answer, CommandContext ctx) { + String fieldTitle = ctx.getDefaultBundle().getString("command.eightball.embed.field_title"); + String fieldDescription = ctx.getDefaultBundle().getString("command.eightball.embed.field_description").replace("{answer}", answer); + return new EmbedBuilder() .setDescription("\"" + question + "\"") - .addField("My Mystical Answer", "\"" + answer + "\"", false) + .addField(fieldTitle, fieldDescription, false) .setColor(Helper.getRandomColor()) .build(); } - private String getAnswer() { - String[] yesAnswers = new String[] { - "It is likely...", - "Probably.", - "Without a doubt.", - "Of course.", - "More than likely.", - "YES ABSOLUTELY.", - "Hmmm... I think so." - }; + private String getAnswer(I18N i18n) { + boolean isYes = Helper.getRandomInteger(0, 2) == 1; - String[] noAnswers = new String[] { - "Of course not.", - "Probably not.", - "It is not likely...", - "There is some doubt...", - "Less than likely.", - "ABSOLUTELY NOT.", - "Are you kidding? No!" - }; + List answers = i18n.getStringArray("command.eightball.answers." + (isYes ? "positive" : "negative")); - if (Helper.getRandomInteger(0, 2) == 1) return yesAnswers[(Helper.getRandomInteger(0, yesAnswers.length))]; - else return noAnswers[(Helper.getRandomInteger(0, noAnswers.length))]; + int answerIndex = Helper.getRandomInteger(0, answers.size()); + return answers.get(answerIndex); } @Override @@ -63,8 +56,8 @@ public String getName() { } @Override - public String getDescription() { - return "Ask me a question!"; + public String getDescriptionPath() { + return "command.eightball.description"; } @Override @@ -75,7 +68,7 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.STRING, "question", "The question you want to ask me!", true) + new OptionData(OptionType.STRING, "question", "command.eightball.arguments.question.description", true) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/SnipeCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/SnipeCommand.java index adf53773d..b4367ea8d 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/SnipeCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/SnipeCommand.java @@ -1,8 +1,10 @@ package com.beanbeanjuice.cafebot.commands.fun; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.types.PotentialSnipeMessage; @@ -11,8 +13,6 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; -import java.awt.*; - public class SnipeCommand extends Command implements ICommand { public SnipeCommand(CafeBot bot) { @@ -20,31 +20,40 @@ public SnipeCommand(CafeBot bot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { bot.getSnipeHandler().popLastMessage(event.getChannelId()).ifPresentOrElse( - (snipedMessage) -> event.getHook().sendMessageEmbeds(this.getSnipedMessageEmbed(snipedMessage)).queue(), - () -> event.getHook().sendMessageEmbeds(this.getNoSnipeMessageEmbed()).queue() + (snipedMessage) -> event.getHook().sendMessageEmbeds(this.getSnipedMessageEmbed(snipedMessage, ctx.getGuildI18n())).queue(), + + () -> event.getHook().sendMessageEmbeds(this.getNoSnipeMessageEmbed(ctx.getGuildI18n())).queue() ); } - private MessageEmbed getSnipedMessageEmbed(PotentialSnipeMessage snipe) { + private MessageEmbed getSnipedMessageEmbed(PotentialSnipeMessage snipe, I18N guildBundle) { EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setTitle("Snipe!"); + String title = guildBundle.getString("command.snipe.embed.snipe.title"); + String description = guildBundle.getString("command.snipe.embed.snipe.description") + .replace("{user}", snipe.getUser().getAsMention()) + .replace("{message}", snipe.getMessage()); + + embedBuilder.setTitle(title); embedBuilder.setAuthor(snipe.getUser().getName(), null, snipe.getUser().getAvatarUrl()); embedBuilder.setColor(Helper.getRandomColor()); - embedBuilder.setDescription(String.format("%s said \"%s\"", snipe.getUser().getAsMention(), snipe.getMessage())); + embedBuilder.setDescription(description); embedBuilder.setTimestamp(snipe.getCreatedAt()); return embedBuilder.build(); } - private MessageEmbed getNoSnipeMessageEmbed() { + private MessageEmbed getNoSnipeMessageEmbed(I18N guildBundle) { EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setTitle("No Snipe Found!"); + String title = guildBundle.getString("command.snipe.embed.none.title"); + String description = guildBundle.getString("command.snipe.embed.none.description"); + + embedBuilder.setTitle(title); embedBuilder.setColor(Helper.getRandomColor()); - embedBuilder.setDescription("No one has deleted any messages in this channel recently..."); + embedBuilder.setDescription(description); return embedBuilder.build(); } @@ -55,8 +64,8 @@ public String getName() { } @Override - public String getDescription() { - return "Snipe a recently deleted message!"; + public String getDescriptionPath() { + return "command.snipe.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayCommand.java index 4395415ed..037f990be 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayCommand.java @@ -16,8 +16,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get someone's birthday or change your own!"; + public String getDescriptionPath() { + return "command.birthday.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayGetSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayGetSubCommand.java index cdc93ecab..ce3ae89c9 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayGetSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayGetSubCommand.java @@ -3,7 +3,9 @@ import com.beanbeanjuice.cafebot.api.wrapper.api.exception.ApiRequestException; import com.beanbeanjuice.cafebot.api.wrapper.type.Birthday; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.logging.LogLevel; @@ -11,6 +13,7 @@ import net.dv8tion.jda.api.entities.MessageEmbed; import net.dv8tion.jda.api.entities.User; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.DiscordLocale; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.OptionData; @@ -20,6 +23,7 @@ import java.time.format.TextStyle; import java.util.Locale; import java.util.Optional; +import java.util.concurrent.CompletionException; public class BirthdayGetSubCommand extends Command implements ISubCommand { @@ -28,71 +32,74 @@ public BirthdayGetSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); boolean isSelf = userMapping.isEmpty(); bot.getCafeAPI().getBirthdayApi().getBirthday(user.getId()) - .thenAccept((birthday) -> { - sendBirthday(user, isSelf, birthday, event); - }) - .exceptionally((ex) -> { - if (ex.getCause() instanceof ApiRequestException apiRequestException) { + .thenAccept((birthday) -> { + sendBirthday(user, isSelf, birthday, event, ctx); + }) + .exceptionally((ex) -> { + handleError(ex, event, ctx); + throw new CompletionException(ex.getCause()); + }); + } - if (apiRequestException.getStatusCode() == HttpStatus.SC_NOT_FOUND) { - sendError(event); - } + private void handleError(Throwable ex, SlashCommandInteractionEvent event, CommandContext ctx) { + if (ex.getCause() instanceof ApiRequestException apiRequestException) { - return null; - } + if (apiRequestException.getStatusCode() == HttpStatus.SC_NOT_FOUND) { + sendError(event, ctx.getUserI18n()); + return; + } - event.getHook().sendMessageEmbeds(Helper.defaultErrorEmbed()).queue(); - bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Getting Birthday: " + ex.getMessage()); + } - return null; - }); + event.getHook().sendMessageEmbeds(Helper.uncaughtErrorEmbed(ctx.getUserI18n(), ex.getMessage())).queue(); + bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Getting Birthday: " + ex.getMessage()); } - private void sendError(SlashCommandInteractionEvent event) { + private void sendError(SlashCommandInteractionEvent event, I18N i18n) { event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "🎂 Error Getting Birthday", - "<:cafeBot_sad:1171726165040447518> Sorry... there was an error getting their birthday. It might not be set, you should tell them to set it with `/birthday`!." + i18n.getString("command.birthday.subcommand.get.embed.error.title"), + i18n.getString("command.birthday.subcommand.get.embed.error.description") )).queue(); } - private void sendBirthday(User user, boolean isSelf, Birthday birthday, SlashCommandInteractionEvent event) { - MessageEmbed embed = (isSelf) ? selfBirthdayEmbed(birthday) : birthdayEmbed(user, birthday); + private void sendBirthday(User user, boolean isSelf, Birthday birthday, SlashCommandInteractionEvent event, CommandContext ctx) { + MessageEmbed embed = (isSelf) ? selfBirthdayEmbed(birthday, ctx.getUserI18n()) : birthdayEmbed(user, birthday, ctx.getUserI18n()); event.getHook().sendMessageEmbeds(embed).queue(); } - private MessageEmbed selfBirthdayEmbed(Birthday birthday) { + private MessageEmbed selfBirthdayEmbed(Birthday birthday, I18N i18n) { + String description = i18n.getString("command.birthday.subcommand.get.embed.self.description") + .replace("{month}", Month.of(birthday.getMonth()).getDisplayName(TextStyle.FULL, Locale.ENGLISH)) + .replace("{day}", String.valueOf(birthday.getDay())) + .replace("{timezone}", birthday.getTimeZone().getId()); + return new EmbedBuilder() .setColor(Helper.getRandomColor()) - .setTitle("🎂 Your Birthday") - .setDescription( - String.format( - "Your birthday is on **%s %d** (%s).", - Month.of(birthday.getMonth()).getDisplayName(TextStyle.FULL, Locale.ENGLISH), - birthday.getDay(), - birthday.getTimeZone().getId() - ) - ) + .setTitle(i18n.getString("command.birthday.subcommand.get.embed.self.title")) + .setDescription(description) .build(); } - private MessageEmbed birthdayEmbed(User user, Birthday birthday) { + private MessageEmbed birthdayEmbed(User user, Birthday birthday, I18N i18n) { + String title = i18n.getString("command.birthday.subcommand.get.embed.other.title") + .replace("{user}", user.getName()); + + String description = i18n.getString("command.birthday.subcommand.get.embed.other.description") + .replace("{month}", Month.of(birthday.getMonth()).getDisplayName(TextStyle.FULL, Locale.ENGLISH)) + .replace("{day}", String.valueOf(birthday.getDay())) + .replace("{timezone}", birthday.getTimeZone().getId()); + return new EmbedBuilder() .setColor(Helper.getRandomColor()) - .setTitle("🎂 " + user.getName() + "'s Birthday") - .setDescription( - String.format("Their birthday is on **%s %d** (%s).", - Month.of(birthday.getMonth()).getDisplayName(TextStyle.FULL, Locale.ENGLISH), - birthday.getDay(), - birthday.getTimeZone().getId() - ) - ) + .setTitle(title) + .setDescription(description) .build(); } @@ -102,14 +109,14 @@ public String getName() { } @Override - public String getDescription() { - return "Get someone's birthday!"; + public String getDescriptionPath() { + return "command.birthday.subcommand.get.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person you want to get the birthday of!", false) + new OptionData(OptionType.USER, "user", "command.birthday.subcommand.get.arguments.user.description", false) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayRemoveSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayRemoveSubCommand.java index 6fcb7405c..5ba0b66a0 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayRemoveSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdayRemoveSubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -14,14 +15,14 @@ public BirthdayRemoveSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { User user = event.getUser(); bot.getCafeAPI().getBirthdayApi().deleteBirthday(user.getId()) .thenRun(() -> { event.getHook().sendMessageEmbeds(Helper.successEmbed( - "Birthday Removed 🥺", - "<:cafeBot_sad:1171726165040447518> Your birthday has been removed... but I know it's sometimes better to keep things private..." + ctx.getUserI18n().getString("command.birthday.subcommand.remove.embed.title"), + ctx.getUserI18n().getString("command.birthday.subcommand.remove.embed.description") )).queue(); }); } @@ -32,8 +33,8 @@ public String getName() { } @Override - public String getDescription() { - return "Remove your birthday."; + public String getDescriptionPath() { + return "command.birthday.subcommand.remove.description"; } } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java index edaad339a..00b594c97 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/birthday/BirthdaySetSubCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.api.wrapper.type.Birthday; import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.logging.LogLevel; @@ -17,6 +18,7 @@ import java.time.format.TextStyle; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; public class BirthdaySetSubCommand extends Command implements ISubCommand { @@ -25,7 +27,7 @@ public BirthdaySetSubCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { String userId = event.getUser().getId(); int month = event.getOption("month").getAsInt(); int day = event.getOption("day").getAsInt(); @@ -36,41 +38,44 @@ public void handle(SlashCommandInteractionEvent event) { bot.getCafeAPI().getBirthdayApi().setBirthday(userId, birthday) .thenAccept((newBirthday) -> { + String description = ctx.getUserI18n().getString("command.birthday.subcommand.set.embed.success.description") + .replace("{month}", Month.of(month).getDisplayName(TextStyle.FULL, Locale.ENGLISH)) + .replace("{day}", String.valueOf(day)) + .replace("{timezone}", zoneId.getId()); event.getHook().sendMessageEmbeds(Helper.successEmbed( - "🎂 Birthday Set", - String.format( - "You have successfully set your birthday to **%s %d** (%s).", - Month.of(month).getDisplayName(TextStyle.FULL, Locale.ENGLISH), - day, - zoneId.getId() - ) + ctx.getUserI18n().getString("command.birthday.subcommand.set.embed.success.title"), + description )).queue(); }) .exceptionally((ex) -> { - event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Error Setting Birthday", - "I... I don't know what happened... the computer's not letting me put your birthday in!" - )).queue(); - - bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Setting Birthday: " + ex.getMessage(), true, true); - return null; + handleError(ex, event, ctx); + throw new CompletionException(ex.getCause()); }); } + private void handleError(Throwable ex, SlashCommandInteractionEvent event, CommandContext ctx) { + event.getHook().sendMessageEmbeds(Helper.errorEmbed( + ctx.getUserI18n().getString("command.birthday.subcommand.set.embed.error.title"), + ctx.getUserI18n().getString("command.birthday.subcommand.set.embed.error.description") + )).queue(); + + bot.getLogger().log(this.getClass(), LogLevel.WARN, "Error Setting Birthday: " + ex.getMessage(), true, true); + } + @Override public String getName() { return "set"; } @Override - public String getDescription() { - return "Edit your birthday!"; + public String getDescriptionPath() { + return "command.birthday.subcommand.set.description"; } @Override public OptionData[] getOptions() { return new OptionData[]{ - new OptionData(OptionType.INTEGER, "month", "The month you were born!", true) + new OptionData(OptionType.INTEGER, "month", "command.birthday.subcommand.set.arguments.month.description", true) .addChoice("1 - January", 1) .addChoice("2 - February", 2) .addChoice("3 - March", 3) @@ -84,12 +89,12 @@ public OptionData[] getOptions() { .addChoice("11 - November", 11) .addChoice("12 - December", 12), - new OptionData(OptionType.INTEGER, "day", "The day you were born in the specified month!", true) + new OptionData(OptionType.INTEGER, "day", "command.birthday.subcommand.set.arguments.day.description", true) .setRequiredRange(1, 31), - new OptionData(OptionType.STRING, "timezone", "Start typing to see available options!", true, true), + new OptionData(OptionType.STRING, "timezone", "command.birthday.subcommand.set.arguments.timezone.description", true, true), - new OptionData(OptionType.INTEGER, "year", "The year you were born in!", false) + new OptionData(OptionType.INTEGER, "year", "command.birthday.subcommand.set.arguments.year.description", false) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/IMemeSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/IMemeSubCommand.java index 95169cf2a..da9443efc 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/IMemeSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/IMemeSubCommand.java @@ -1,5 +1,6 @@ package com.beanbeanjuice.cafebot.commands.fun.meme; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.meme.api.wrapper.MemeAPI; @@ -14,15 +15,15 @@ public interface IMemeSubCommand extends ISubCommand { String[] getSubreddits(); - default void handle(SlashCommandInteractionEvent event) { + default void handle(SlashCommandInteractionEvent event, CommandContext ctx) { int subredditIndex = Helper.getRandomInteger(0, this.getSubreddits().length); MemeAPI.get(this.getSubreddits()[subredditIndex]) .thenAccept((redditMeme) -> event.getHook().sendMessageEmbeds(this.getMessageEmbed(redditMeme)).queue()) .exceptionally((ex) -> { event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Failed Getting Meme", - "I... I'm so sorry... I tripped and fell when getting your meme..." + ctx.getUserI18n().getString("command.meme.description.error.embed.title"), + ctx.getUserI18n().getString("command.meme.description.error.embed.description") )).queue(); throw new CompletionException(ex); }); diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCoffeeSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCoffeeSubCommand.java index c81d5f8c1..e6f2d2998 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCoffeeSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCoffeeSubCommand.java @@ -24,8 +24,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get a coffee meme!"; + public String getDescriptionPath() { + return "command.meme.subcommand.coffee.description"; } } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCommand.java index 01f9832c2..45a89f274 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeCommand.java @@ -19,8 +19,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get a meme!"; + public String getDescriptionPath() { + return "command.meme.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeRandomSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeRandomSubCommand.java index 90a8b35e3..5a3ac9359 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeRandomSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeRandomSubCommand.java @@ -26,8 +26,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get a random meme!"; + public String getDescriptionPath() { + return "command.meme.subcommand.random.description"; } } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeTeaSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeTeaSubCommand.java index 1611f42fc..5291873b4 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeTeaSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/meme/MemeTeaSubCommand.java @@ -23,8 +23,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get a tea meme!"; + public String getDescriptionPath() { + return "command.meme.subcommand.tea.description"; } } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCaffeinatedSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCaffeinatedSubCommand.java index a5c288a2a..3c13b0663 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCaffeinatedSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCaffeinatedSubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -19,15 +20,19 @@ public RateCaffeinatedSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); int percentage = Helper.getRandomInteger(0, 101); + String description = ctx.getGuildI18n().getString("command.rate.subcommand.caffeinated.embed.description") + .replace("{user}", user.getAsMention()) + .replace("{percent}", String.valueOf(percentage)); + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "☕ Caffeine Rating ☕", - String.format("%s is %d%% caffeinated! %s", user.getAsMention(), percentage, "") + ctx.getGuildI18n().getString("command.rate.subcommand.caffeinated.embed.title"), + description )).mention(user).queue(); } @@ -37,14 +42,14 @@ public String getName() { } @Override - public String getDescription() { - return "Rate how caffeinated you or someone is!"; + public String getDescriptionPath() { + return "command.rate.subcommand.caffeinated.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's caffeine levels you want to see.") + new OptionData(OptionType.USER, "user", "command.rate.subcommand.caffeinated.arguments.user.description") }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCommand.java index 525f50387..0353f7159 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateCommand.java @@ -19,8 +19,8 @@ public String getName() { } @Override - public String getDescription() { - return "Rate something!"; + public String getDescriptionPath() { + return "command.rate.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateGaySubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateGaySubCommand.java index 496f42f41..c19c259d5 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateGaySubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateGaySubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -19,15 +20,19 @@ public RateGaySubCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); int percentage = Helper.getRandomInteger(0, 101); + String description = ctx.getGuildI18n().getString("command.rate.subcommand.gay.embed.description") + .replace("{user}", user.getAsMention()) + .replace("{percent}", String.valueOf(percentage)); + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "🏳️‍🌈 Gay Rating 🏳️‍🌈", - String.format("%s is %d%% gay! <:bloatedBlush:1154475611893547218>", user.getAsMention(), percentage) + ctx.getGuildI18n().getString("command.rate.subcommand.gay.embed.title"), + description )).mention(user).queue(); } @@ -37,14 +42,14 @@ public String getName() { } @Override - public String getDescription() { - return "See how colorful someone is!"; + public String getDescriptionPath() { + return "command.rate.subcommand.gay.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's gayness you want to see.") + new OptionData(OptionType.USER, "user", "command.rate.subcommand.gay.arguments.user.description") }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateInsaneSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateInsaneSubCommand.java index fd49113a7..8332e01c7 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateInsaneSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateInsaneSubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -19,15 +20,19 @@ public RateInsaneSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); int percentage = Helper.getRandomInteger(0, 101); + String description = ctx.getGuildI18n().getString("command.rate.subcommand.insane.embed.description") + .replace("{user}", user.getAsMention()) + .replace("{percent}", String.valueOf(percentage)); + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "‼️ Insane Rating ‼️", - String.format("%s is %d%% insane! %s", user.getAsMention(), percentage, "") + ctx.getGuildI18n().getString("command.rate.subcommand.insane.embed.title"), + description )).mention(user).queue(); } @@ -37,14 +42,14 @@ public String getName() { } @Override - public String getDescription() { - return "See how insane someone is!"; + public String getDescriptionPath() { + return "command.rate.subcommand.insane.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's insanity you want to see.") + new OptionData(OptionType.USER, "user", "command.rate.subcommand.insane.arguments.user.description") }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RatePoorSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RatePoorSubCommand.java index b284cffc1..cf54c7c47 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RatePoorSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RatePoorSubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -19,15 +20,19 @@ public RatePoorSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); int percentage = Helper.getRandomInteger(0, 101); + String description = ctx.getGuildI18n().getString("command.rate.subcommand.poor.embed.description") + .replace("{user}", user.getAsMention()) + .replace("{percent}", String.valueOf(percentage)); + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "💸 Poor Rating 💸", - String.format("%s is %d%% poor! %s", user.getAsMention(), percentage, "🤢🤮") + ctx.getGuildI18n().getString("command.rate.subcommand.poor.embed.title"), + description )).mention(user).queue(); } @@ -37,14 +42,14 @@ public String getName() { } @Override - public String getDescription() { - return "Rate how poor someone is!"; + public String getDescriptionPath() { + return "command.rate.subcommand.poor.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's poor level you want to see.") + new OptionData(OptionType.USER, "user", "command.rate.subcommand.poor.arguments.user.description") }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSimpSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSimpSubCommand.java index f2f5ae670..852f1da59 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSimpSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSimpSubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -19,15 +20,19 @@ public RateSimpSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); int percentage = Helper.getRandomInteger(0, 101); + String description = ctx.getGuildI18n().getString("command.rate.subcommand.simp.embed.description") + .replace("{user}", user.getAsMention()) + .replace("{percent}", String.valueOf(percentage)); + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "😍 Simp Rating 😍", - String.format("%s is %d%% simp! %s", user.getAsMention(), percentage, "<:flushed_nervous:841923862202548224>") + ctx.getGuildI18n().getString("command.rate.subcommand.simp.embed.title"), + description )).mention(user).queue(); } @@ -37,14 +42,14 @@ public String getName() { } @Override - public String getDescription() { - return "Rate how much of a simp someone is!"; + public String getDescriptionPath() { + return "command.rate.subcommand.simp.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's simp level you want to see.") + new OptionData(OptionType.USER, "user", "command.rate.subcommand.simp.arguments.user.description") }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSmartSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSmartSubCommand.java index 0193abcd0..b94501fcb 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSmartSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/fun/rate/RateSmartSubCommand.java @@ -2,6 +2,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.entities.User; @@ -19,15 +20,19 @@ public RateSmartSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); int percentage = Helper.getRandomInteger(0, 101); + String description = ctx.getGuildI18n().getString("command.rate.subcommand.smart.embed.description") + .replace("{user}", user.getAsMention()) + .replace("{percent}", String.valueOf(percentage)); + event.getHook().sendMessageEmbeds(Helper.successEmbed( - "<:smartPeepo:1000248538376196280> Smart Rating <:smartPeepo:1000248538376196280>", - String.format("%s is %d%% smart! %s", user.getAsMention(), percentage, "<:bloatedBlush:1154475611893547218>") + ctx.getGuildI18n().getString("command.rate.subcommand.smart.embed.title"), + description )).mention(user).queue(); } @@ -37,14 +42,14 @@ public String getName() { } @Override - public String getDescription() { - return "Rate how smart someone is!"; + public String getDescriptionPath() { + return "command.rate.subcommand.smart.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's smart levels you want to see.") + new OptionData(OptionType.USER, "user", "command.rate.subcommand.smart.arguments.user.description") }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/CoinFlipCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/CoinFlipCommand.java index abcbdfbb5..a592f7b1d 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/games/CoinFlipCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/CoinFlipCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.Permission; @@ -15,10 +16,15 @@ public CoinFlipCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { boolean isHeads = (Helper.getRandomInteger(0, 2) == 1); - String coin = (isHeads) ? "HEADS" : "TAILS"; - event.getHook().sendMessageEmbeds(Helper.descriptionEmbed(String.format("The coin is **%s**!", coin))).queue(); + + String path = "command.coinflip.coin." + (isHeads ? "heads" : "tails"); + String coin = ctx.getGuildI18n().getString(path); + + String description = ctx.getGuildI18n().getString("command.coinflip.embed.description").replace("{side}", coin); + + event.getHook().sendMessageEmbeds(Helper.descriptionEmbed(description)).queue(); } @Override @@ -27,8 +33,8 @@ public String getName() { } @Override - public String getDescription() { - return "Flip a coin!"; + public String getDescriptionPath() { + return "command.coinflip.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/CountingStatisticsCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/CountingStatisticsCommand.java index b65689359..b40ecb400 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/games/CountingStatisticsCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/CountingStatisticsCommand.java @@ -2,8 +2,10 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.api.wrapper.type.CountingStatistics; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import com.beanbeanjuice.cafebot.utility.logging.LogLevel; @@ -14,10 +16,7 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import org.jetbrains.annotations.Nullable; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.CompletionException; import java.util.stream.Collectors; @@ -28,7 +27,7 @@ public CountingStatisticsCommand(CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Guild guild = event.getGuild(); String guildId = guild.getId(); @@ -37,12 +36,12 @@ public void handle(SlashCommandInteractionEvent event) { int currentRanking = getCurrentGlobalRanking(globalCountingMap, guildId); CountingStatistics guildStatistics = globalCountingMap.get(guild.getId()); - event.getHook().sendMessageEmbeds(getStatisticsEmbed(guildStatistics, highestRanking, currentRanking, globalCountingMap.size())).queue(); + event.getHook().sendMessageEmbeds(getStatisticsEmbed(guildStatistics, highestRanking, currentRanking, globalCountingMap.size(), ctx.getUserI18n())).queue(); }).exceptionally((ex) -> { - event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Counting Statistics Error", - "I'm really.. sorry... I'm having trouble getting the global counting statistics..." - )).queue(); + String title = ctx.getUserI18n().getString("command.counting-statistics.embed.error.title"); + String description = ctx.getUserI18n().getString("command.counting-statistics.embed.error.description"); + + event.getHook().sendMessageEmbeds(Helper.errorEmbed(title, description)).queue(); bot.getLogger().log(this.getClass(), LogLevel.WARN, String.format("Error Getting Counting Statistics: %s", ex.getMessage())); throw new CompletionException(ex); @@ -77,33 +76,28 @@ private int getHighestGlobalRanking(Map globalCounti return ranking; } - private MessageEmbed getStatisticsEmbed(@Nullable CountingStatistics statistics, int highestRanking, int currentRanking, int numGuildsInRanking) { + private MessageEmbed getStatisticsEmbed(@Nullable CountingStatistics statistics, int highestRanking, int currentRanking, int numGuildsInRanking, I18N userBundle) { EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setTitle("Global Counting Statistics ➕"); + String title = userBundle.getString("command.counting-statistics.embed.stats.title"); + String description = userBundle.getString("command.counting-statistics.embed.stats.description") + .replace("{num_guilds}", String.valueOf(numGuildsInRanking)) + .replace("{highest}", String.valueOf(highestRanking)) + .replace("{current}", String.valueOf(currentRanking)); + + embedBuilder.setTitle(title); embedBuilder.setColor(Helper.getRandomColor()); StringBuilder sb = new StringBuilder(); - sb.append(String.format(""" - ***Highest Ranking***: %d/%d - *This is the ranking for your* ***highest*** *number.* - - ***Current Ranking***: %d/%d - *This is the ranking for your* ***current*** *number.* - """, - highestRanking, numGuildsInRanking, - currentRanking, numGuildsInRanking) - ); + sb.append(description); if (statistics != null) { - sb.append(String.format(""" - - Your highest number is **%d**! - Your current number is **%d**! This means your next number should be **%d**... - """, - statistics.getHighestCount(), - statistics.getCurrentCount(), statistics.getCurrentCount() + 1) - ); + String extendedDescription = userBundle.getString("command.counting-statistics.embed.stats.extended-description") + .replace("{highest}", String.valueOf(statistics.getHighestCount())) + .replace("{current}", String.valueOf(statistics.getCurrentCount())) + .replace("{next}", String.valueOf(statistics.getCurrentCount() + 1)); + + sb.append(extendedDescription); } embedBuilder.setDescription(sb.toString()); @@ -117,8 +111,8 @@ public String getName() { } @Override - public String getDescription() { - return "Get the server's global counting statistics!"; + public String getDescriptionPath() { + return "command.counting-statistics.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/RollCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/RollCommand.java index df846dc48..d433b7de4 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/games/RollCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/RollCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.Permission; @@ -20,11 +21,14 @@ public RollCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional sizeMapping = Optional.ofNullable(event.getOption("size")); int size = sizeMapping.map(OptionMapping::getAsInt).orElse(6); int roll = Helper.getRandomInteger(1, size + 1); - event.getHook().sendMessageEmbeds(Helper.descriptionEmbed(String.format("You rolled a **%d**!", roll))).queue(); + + String description = ctx.getGuildI18n().getString("command.roll.roll").replace("{number}", String.valueOf(roll)); + + event.getHook().sendMessageEmbeds(Helper.descriptionEmbed(description)).queue(); } @Override @@ -33,8 +37,8 @@ public String getName() { } @Override - public String getDescription() { - return "Roll a pair of dice!"; + public String getDescriptionPath() { + return "command.roll.description"; } @Override @@ -45,8 +49,8 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.INTEGER, "size", "The size of the die.", false) - .setRequiredRange(1, 10000) + new OptionData(OptionType.INTEGER, "size", "command.roll.arguments.size.description", false) + .setRequiredRange(1, Integer.MAX_VALUE) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java index 467b151e3..3edc1ecc6 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/TicTacToeCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.handlers.games.TicTacToeHandler; import com.beanbeanjuice.cafebot.utility.helper.Helper; @@ -26,16 +27,16 @@ public TicTacToeCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { int wager = Optional.ofNullable(event.getOption("wager")).map(OptionMapping::getAsInt).orElse(0); User opponent = event.getOption("opponent").getAsUser(); if (opponent.isBot()) { - event.getHook().sendMessageEmbeds(Helper.errorEmbed( - "Cannot Play Against Bot", - "What do you think you're even doing? <:cafeBot_angry:1171726164092518441>" - )).queue(); + String title = ctx.getUserI18n().getString("command.tictactoe.embeds.bot.title"); + String description = ctx.getUserI18n().getString("command.tictactoe.embeds.bot.description"); + + event.getHook().sendMessageEmbeds(Helper.errorEmbed(title, description)).queue(); return; } @@ -43,12 +44,14 @@ public void handle(SlashCommandInteractionEvent event) { String playerId = player.getId(); String opponentId = opponent.getId(); - event.getHook().sendMessageEmbeds( - Helper.successEmbed( - "Waiting for Consent", - String.format("%s, do you accept the challenge from %s for **%d cC** (cafeCoins)? Once accepted, games will be automatically closed after 24 hours.", opponent.getAsMention(), player.getAsMention(), wager) - ) - ) + String title = ctx.getUserI18n().getString("command.tictactoe.embeds.consent.title"); + String description = ctx.getUserI18n().getString("command.tictactoe.embeds.consent.description") + .replace("{player1}", opponent.getAsMention()) + .replace("{player2}", player.getAsMention()) + .replace("{wager}", String.valueOf(wager)); + + event.getHook() + .sendMessageEmbeds(Helper.successEmbed(title, description)) .addComponents(ActionRow.of(Button.primary(String.format(TicTacToeHandler.CONSENT_BUTTON_ID_FULL, playerId, opponentId, wager), Emoji.fromUnicode("🤝")))) .queue(); } @@ -59,8 +62,8 @@ public String getName() { } @Override - public String getDescription() { - return "Play tic-tac-toe with someone!"; + public String getDescriptionPath() { + return "command.tictactoe.description"; } @Override @@ -71,8 +74,8 @@ public CommandCategory getCategory() { @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "opponent", "The person you want to play against.", true), - new OptionData(OptionType.INTEGER, "wager", "The amount you want to wager for this game.", false) + new OptionData(OptionType.USER, "opponent", "command.tictactoe.arguments.opponent.description", true), + new OptionData(OptionType.INTEGER, "wager", "command.tictactoe.arguments.wager.description", false) .setMinValue(1) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameCommand.java index 31a8aa111..973d6f1ab 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameCommand.java @@ -19,8 +19,8 @@ public String getName() { } @Override - public String getDescription() { - return "Something to do with games!"; + public String getDescriptionPath() { + return "command.game.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameStatsSubCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameStatsSubCommand.java index 2edd23f20..f0c3c2883 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameStatsSubCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/games/game/GameStatsSubCommand.java @@ -3,7 +3,9 @@ import com.beanbeanjuice.cafebot.api.wrapper.api.enums.GameStatusType; import com.beanbeanjuice.cafebot.api.wrapper.api.enums.GameType; import com.beanbeanjuice.cafebot.CafeBot; +import com.beanbeanjuice.cafebot.i18n.I18N; import com.beanbeanjuice.cafebot.utility.commands.Command; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ISubCommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.EmbedBuilder; @@ -25,7 +27,7 @@ public GameStatsSubCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { Optional userMapping = Optional.ofNullable(event.getOption("user")); User user = userMapping.map(OptionMapping::getAsUser).orElse(event.getUser()); @@ -64,7 +66,7 @@ public void handle(SlashCommandInteractionEvent event) { gameLosses.put(game.getType(), currentLosses); }); - event.getHook().sendMessageEmbeds(gameStatisticsEmbed(user, gamesPlayed, gamesWon, gamesLost, gamesPlayedByType, gameWins, gameLosses)).queue(); + event.getHook().sendMessageEmbeds(gameStatisticsEmbed(user, gamesPlayed, gamesWon, gamesLost, gamesPlayedByType, gameWins, gameLosses, ctx.getGuildI18n())).queue(); }); @@ -77,23 +79,38 @@ private MessageEmbed gameStatisticsEmbed( int gamesLost, HashMap gameTotals, HashMap gameWins, - HashMap gameLosses + HashMap gameLosses, + I18N bundle ) { + String title = bundle.getString("command.game.subcommands.stats.embed.title").replace("{user}", user.getName()); + String description = bundle.getString("command.game.subcommands.stats.embed.stats"); + + String totalsTitle = bundle.getString("command.game.subcommands.stats.embed.fields.total.name"); + String totalsDescription = description + .replace("{wins}", String.valueOf(gamesWon)) + .replace("{losses}", String.valueOf(gamesLost)) + .replace("{played}", String.valueOf(gamesPlayed)); + EmbedBuilder embedBuilder = new EmbedBuilder(); - embedBuilder.setAuthor(String.format("%s's Game Statistics", user.getName()), null, user.getAvatarUrl()); + embedBuilder.setAuthor(title, null, user.getAvatarUrl()); - embedBuilder.addField("**Totals** 🎯", String.format("**Wins**: %s\n**Losses**: %s\n**Played**: %s", gamesWon, gamesLost, gamesPlayed), true); + embedBuilder.addField(totalsTitle, totalsDescription, true); Arrays.stream(GameType.values()).forEach((type) -> { + String gameDescription = description + .replace("{wins}", String.valueOf(gameWins.get(type))) + .replace("{losses}", String.valueOf(gameLosses.get(type))) + .replace("{played}", String.valueOf(gameTotals.get(type))); + embedBuilder.addField( String.format("**%s**", type.getFriendlyName()), - String.format("**Wins**: %s\n**Losses**: %s\n**Played**: %s", gameWins.get(type), gameLosses.get(type), gameTotals.get(type)), + gameDescription, true ); }); embedBuilder.setColor(Helper.getRandomColor()); - embedBuilder.setFooter("Do you want to play a game?~"); + embedBuilder.setFooter(bundle.getString("command.game.subcommands.stats.embed.footer")); return embedBuilder.build(); } @@ -104,14 +121,14 @@ public String getName() { } @Override - public String getDescription() { - return "Get someone's game stats!"; + public String getDescriptionPath() { + return "command.game.subcommands.stats.description"; } @Override public OptionData[] getOptions() { return new OptionData[] { - new OptionData(OptionType.USER, "user", "The person who's game data you want to see.", false) + new OptionData(OptionType.USER, "user", "command.game.subcommands.stats.arguments.user.description", false) }; } diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotDonateCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotDonateCommand.java index f92a03666..35a26f968 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotDonateCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotDonateCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.Permission; @@ -15,17 +16,11 @@ public BotDonateCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { - event.getHook().sendMessageEmbeds( - Helper.successEmbed( - "Donations!", - """ - Donations are absolutely optional, but they help keep me alive! \ - You can donate [here](https://buymeacoffee.com/beanbeanjuice)! \ - Thank you so much... - """ - ) - ).queue(); + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { + String title = ctx.getDefaultBundle().getString("command.bot-donate.success.title"); + String description = ctx.getDefaultBundle().getString("command.bot-donate.success.description"); + + event.getHook().sendMessageEmbeds(Helper.successEmbed(title, description)).queue(); } @Override @@ -34,8 +29,8 @@ public String getName() { } @Override - public String getDescription() { - return "Donate to keep the bot up!"; + public String getDescriptionPath() { + return "command.bot-donate.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotInviteCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotInviteCommand.java index b8b2a2fb2..d23f588ea 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotInviteCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotInviteCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.components.actionrow.ActionRow; @@ -17,7 +18,7 @@ public BotInviteCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { event.getHook().sendMessageComponents(ActionRow.of(getInviteButton())).queue(); } @@ -33,8 +34,8 @@ public String getName() { } @Override - public String getDescription() { - return "Want to invite this bot to a server? Use this command!"; + public String getDescriptionPath() { + return "command.bot-invite.description"; } @Override diff --git a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotUpvoteCommand.java b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotUpvoteCommand.java index 3d05ca8e6..b9c40f513 100644 --- a/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotUpvoteCommand.java +++ b/src/main/java/com/beanbeanjuice/cafebot/commands/generic/BotUpvoteCommand.java @@ -3,6 +3,7 @@ import com.beanbeanjuice.cafebot.CafeBot; import com.beanbeanjuice.cafebot.utility.commands.Command; import com.beanbeanjuice.cafebot.utility.commands.CommandCategory; +import com.beanbeanjuice.cafebot.utility.commands.CommandContext; import com.beanbeanjuice.cafebot.utility.commands.ICommand; import com.beanbeanjuice.cafebot.utility.helper.Helper; import net.dv8tion.jda.api.Permission; @@ -20,15 +21,11 @@ public BotUpvoteCommand(final CafeBot cafeBot) { } @Override - public void handle(SlashCommandInteractionEvent event) { - event.getHook().sendMessageEmbeds(Helper.successEmbed( - "Voting List", - """ - Upvoting helps me serve coffee to more people! \ - If you're enjoying my service, please consider doing so! \ - Any and all support is welcome! - """ - )).setComponents(ActionRow.of(getButtons())).queue(); + public void handle(SlashCommandInteractionEvent event, CommandContext ctx) { + String title = ctx.getUserI18n().getString("command.bot-upvote.embed.title"); + String description = ctx.getUserI18n().getString("command.bot-upvote.embed.description"); + + event.getHook().sendMessageEmbeds(Helper.successEmbed(title, description)).setComponents(ActionRow.of(getButtons())).queue(); } private ArrayList