diff --git a/build.gradle b/build.gradle index c96df3a1..4f0722ba 100644 --- a/build.gradle +++ b/build.gradle @@ -114,6 +114,7 @@ dependencies { } testImplementation 'org.jetbrains.kotlin:kotlin-test' testImplementation 'com.natpryce:hamkrest:1.8.0.1' + testImplementation 'org.mockito.kotlin:mockito-kotlin:4.1.0' // Old Tests oldTestImplementation 'org.spigotmc:spigot-api:1.19.3-R0.1-SNAPSHOT' @@ -241,7 +242,7 @@ javadoc { project.configurations.api.canBeResolved = true shadowJar { - relocate 'co.aikar', 'com.onarandombox.acf' + relocate 'co.aikar.commands', 'com.onarandombox.acf' relocate 'com.dumptruckman.minecraft.util.Logging', 'com.onarandombox.MultiverseCore.utils.CoreLogging' relocate 'com.dumptruckman.minecraft.util.DebugLog', 'com.onarandombox.MultiverseCore.utils.DebugFileLogger' relocate 'de.themoep.idconverter', 'com.onarandombox.idconverter' diff --git a/src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java b/src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java new file mode 100644 index 00000000..3d136f3a --- /dev/null +++ b/src/main/java/co/aikar/commands/OpenBukkitCommandIssuer.java @@ -0,0 +1,13 @@ +package co.aikar.commands; + +import org.bukkit.command.CommandSender; + +/** + * Exists just so we can extend BukkitCommandIssuer since it has a package-private constructor. + */ +public abstract class OpenBukkitCommandIssuer extends BukkitCommandIssuer { + + protected OpenBukkitCommandIssuer(BukkitCommandManager manager, CommandSender sender) { + super(manager, sender); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java b/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java index e7274072..f4dc5563 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java +++ b/src/main/java/com/onarandombox/MultiverseCore/MultiverseCore.java @@ -23,6 +23,7 @@ import com.onarandombox.MultiverseCore.api.MVWorld; import com.onarandombox.MultiverseCore.api.MVWorldManager; import com.onarandombox.MultiverseCore.commandtools.MVCommandManager; import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; import com.onarandombox.MultiverseCore.config.MVCoreConfig; import com.onarandombox.MultiverseCore.destination.DestinationsProvider; import com.onarandombox.MultiverseCore.economy.MVEconomist; @@ -68,6 +69,8 @@ public class MultiverseCore extends JavaPlugin implements MVCore { private Provider metricsConfiguratorProvider; @Inject private Provider economistProvider; + @Inject + private Provider pluginLocalesProvider; // Counter for the number of plugins that have registered with us private int pluginCount; @@ -129,8 +132,8 @@ public class MultiverseCore extends JavaPlugin implements MVCore { // Init all the other stuff this.loadAnchors(); this.registerEvents(); - this.registerCommands(); this.setUpLocales(); + this.registerCommands(); this.registerDestinations(); this.setupMetrics(); this.loadPlaceholderAPIIntegration(); @@ -173,13 +176,19 @@ public class MultiverseCore extends JavaPlugin implements MVCore { private void loadEconomist() { Try.run(() -> economistProvider.get()) - .onFailure(e -> Logging.severe("Failed to load economy integration", e)); + .onFailure(e -> { + Logging.severe("Failed to load economy integration"); + e.printStackTrace(); + }); } private void loadAnchors() { Try.of(() -> anchorManagerProvider.get()) .onSuccess(AnchorManager::loadAnchors) - .onFailure(e -> Logging.severe("Failed to load anchors", e)); + .onFailure(e -> { + Logging.severe("Failed to load anchors"); + e.printStackTrace(); + }); } /** @@ -204,7 +213,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { serviceLocator.getAllServices(MultiverseCommand.class) .forEach(commandManager::registerCommand); }) - .onFailure(e -> Logging.severe("Failed to register commands", e)); + .onFailure(e -> { + Logging.severe("Failed to register commands"); + e.printStackTrace(); + }); } /** @@ -212,12 +224,18 @@ public class MultiverseCore extends JavaPlugin implements MVCore { */ private void setUpLocales() { Try.of(() -> commandManagerProvider.get()) - .andThenTry(commandManager -> { + .andThen(commandManager -> { commandManager.usePerIssuerLocale(true, true); - commandManager.getLocales().addFileResClassLoader(this); - commandManager.getLocales().addMessageBundles("multiverse-core"); }) - .onFailure(e -> Logging.severe("Failed to register locales", e)); + .mapTry(commandManager -> pluginLocalesProvider.get()) + .andThen(pluginLocales -> { + pluginLocales.addFileResClassLoader(this); + pluginLocales.addMessageBundles("multiverse-core"); + }) + .onFailure(e -> { + Logging.severe("Failed to register locales"); + e.printStackTrace(); + }); } /** @@ -229,7 +247,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { serviceLocator.getAllServices(Destination.class) .forEach(destinationsProvider::registerDestination); }) - .onFailure(e -> Logging.severe("Failed to register destinations", e)); + .onFailure(e -> { + Logging.severe("Failed to register destinations"); + e.printStackTrace(); + }); } /** @@ -239,7 +260,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { if (TestingMode.isDisabled()) { // Load metrics Try.of(() -> metricsConfiguratorProvider.get()) - .onFailure(e -> Logging.severe("Failed to setup metrics", e)); + .onFailure(e -> { + Logging.severe("Failed to setup metrics"); + e.printStackTrace(); + }); } else { Logging.info("Metrics are disabled in testing mode."); } @@ -261,7 +285,10 @@ public class MultiverseCore extends JavaPlugin implements MVCore { if (config.isRegisterPapiHook() && getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) { Try.run(() -> serviceLocator.createAndInitialize(MultiverseCorePlaceholders.class)) - .onFailure(e -> Logging.severe("Failed to load PlaceholderAPI integration.", e)); + .onFailure(e -> { + Logging.severe("Failed to load PlaceholderAPI integration."); + e.printStackTrace(); + }); } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java new file mode 100644 index 00000000..f45b3a71 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandIssuer.java @@ -0,0 +1,48 @@ +package com.onarandombox.MultiverseCore.commandtools; + +import co.aikar.commands.MessageType; +import co.aikar.commands.OpenBukkitCommandIssuer; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.message.Message; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +public class MVCommandIssuer extends OpenBukkitCommandIssuer { + + private final MVCommandManager commandManager; + + MVCommandIssuer(@NotNull MVCommandManager commandManager, @NotNull CommandSender sender) { + super(commandManager, sender); + this.commandManager = commandManager; + } + + @Override + public MVCommandManager getManager() { + return commandManager; + } + + public void sendError(Message message) { + sendMessage(MessageType.ERROR, message); + } + + public void sendSyntax(Message message) { + sendMessage(MessageType.SYNTAX, message); + } + + public void sendInfo(Message message) { + sendMessage(MessageType.INFO, message); + } + + private void sendMessage(MessageType messageType, Message message) { + if (message instanceof MessageKeyProvider) { + sendMessage(messageType, (MessageKeyProvider) message, message.getReplacements()); + } else { + var formatter = getManager().getFormat(messageType); + if (formatter != null) { + sendMessage(formatter.format(message.formatted())); + } else { + sendMessage(message.formatted()); + } + } + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java index b64236ce..b3880933 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/MVCommandManager.java @@ -4,6 +4,7 @@ import java.util.List; import co.aikar.commands.BukkitCommandCompletionContext; import co.aikar.commands.BukkitCommandExecutionContext; +import co.aikar.commands.BukkitLocales; import co.aikar.commands.CommandCompletions; import co.aikar.commands.CommandContexts; import co.aikar.commands.CommandHelp; @@ -15,6 +16,8 @@ import com.onarandombox.MultiverseCore.commandtools.flags.CommandFlagsManager; import com.onarandombox.MultiverseCore.commandtools.queue.CommandQueueManager; import jakarta.inject.Inject; import jakarta.inject.Provider; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; import org.jetbrains.annotations.NotNull; import org.jvnet.hk2.annotations.Service; @@ -28,7 +31,6 @@ public class MVCommandManager extends PaperCommandManager { private final CommandQueueManager commandQueueManager; private final Provider commandContextsProvider; private final Provider commandCompletionsProvider; - private PluginLocales pluginLocales; @Inject public MVCommandManager( @@ -48,6 +50,18 @@ public class MVCommandManager extends PaperCommandManager { MVCommandConditions.load(this, worldManager); } + void loadLanguages(PluginLocales locales) { + if (this.locales == null) { + this.locales = locales; + this.locales.loadLanguages(); + } + } + + @Override + public BukkitLocales getLocales() { + return this.locales; + } + /** * Gets class responsible for flag handling. * @@ -57,21 +71,6 @@ public class MVCommandManager extends PaperCommandManager { return flagsManager; } - /** - * Gets class responsible for locale handling. - * - * @return A not-null {@link PluginLocales}. - */ - @Override - public PluginLocales getLocales() { - if (this.pluginLocales == null) { - this.pluginLocales = new PluginLocales(this); - this.locales = pluginLocales; // For parent class - this.pluginLocales.loadLanguages(); - } - return this.pluginLocales; - } - /** * Manager for command that requires /mv confirm before execution. * @@ -120,4 +119,17 @@ public class MVCommandManager extends PaperCommandManager { } help.showHelp(); } + + public @NotNull MVCommandIssuer getConsoleCommandIssuer() { + return getCommandIssuer(Bukkit.getConsoleSender()); + } + + @Override + public @NotNull MVCommandIssuer getCommandIssuer(Object issuer) { + if (!(issuer instanceof CommandSender)) { + throw new IllegalArgumentException(issuer.getClass().getName() + " is not a Command Issuer."); + } else { + return new MVCommandIssuer(this, (CommandSender)issuer); + } + } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java b/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java index 66c0f8c1..2db5686f 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java +++ b/src/main/java/com/onarandombox/MultiverseCore/commandtools/PluginLocales.java @@ -1,14 +1,16 @@ package com.onarandombox.MultiverseCore.commandtools; -import co.aikar.commands.BukkitCommandManager; import co.aikar.commands.BukkitLocales; import com.onarandombox.MultiverseCore.utils.file.FileResClassLoader; +import jakarta.inject.Inject; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; +import org.jvnet.hk2.annotations.Service; /** * Locale manager with additional methods for loading locales from plugin's locales folder. */ +@Service public class PluginLocales extends BukkitLocales { private static final String DEFAULT_LOCALE_FOLDER_PATH = "locales"; @@ -18,8 +20,10 @@ public class PluginLocales extends BukkitLocales { * * @param manager The command manager. */ - public PluginLocales(BukkitCommandManager manager) { + @Inject + public PluginLocales(MVCommandManager manager) { super(manager); + manager.loadLanguages(this); } /** diff --git a/src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java b/src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java new file mode 100644 index 00000000..268468fd --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/exceptions/MultiverseException.java @@ -0,0 +1,52 @@ +package com.onarandombox.MultiverseCore.exceptions; + +import com.onarandombox.MultiverseCore.commandtools.MVCommandIssuer; +import com.onarandombox.MultiverseCore.utils.message.Message; +import org.jetbrains.annotations.Nullable; + +/** + * A base exception for Multiverse. + *
+ * {@link #getMVMessage()} provides access to a {@link Message} which can be used to provide a localized message. See + * {@link MVCommandIssuer#sendInfo(Message)}. + */ +public class MultiverseException extends Exception { + + private final @Nullable Message message; + + /** + * Creates a new exception with the given message and cause. + *
+ * If the message is not null, this exception will also contain a {@link Message} which can be accessed via + * {@link #getMVMessage()}. This message will just be the given message wrapped in a {@link Message}. + * + * @param message The message for the exception + * @param cause The cause of the exception + */ + public MultiverseException(@Nullable String message, @Nullable Throwable cause) { + this(message != null ? Message.of(message) : null, cause); + } + + /** + * Creates a new exception with the given message and cause. + *
+ * If the message is not null, this exception will also contain a String message which can be accessed via + * {@link #getMessage()}. This message will just be the given message formatted without locale support. + * + * @param message The message for the exception + * @param cause The cause of the exception + */ + public MultiverseException(@Nullable Message message, @Nullable Throwable cause) { + super(message != null ? message.formatted() : null, cause); + this.message = message; + } + + /** + * Gets the {@link Message} for this exception. + * + * @return The message, or null if none was provided + */ + public final @Nullable Message getMVMessage() { + return message; + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java b/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java index 1dc79b9a..1e5cbe35 100644 --- a/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/MVCorei18n.java @@ -2,6 +2,9 @@ package com.onarandombox.MultiverseCore.utils; import co.aikar.locales.MessageKey; import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.utils.message.Message; +import com.onarandombox.MultiverseCore.utils.message.MessageReplacement; +import org.jetbrains.annotations.NotNull; public enum MVCorei18n implements MessageKeyProvider { // config status @@ -84,4 +87,9 @@ public enum MVCorei18n implements MessageKeyProvider { public MessageKey getMessageKey() { return this.key; } + + @NotNull + public Message bundle(@NotNull String nonLocalizedMessage, @NotNull MessageReplacement... replacements) { + return Message.of(this, nonLocalizedMessage, replacements); + } } diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java new file mode 100644 index 00000000..fcf77f7a --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/LocalizedMessage.java @@ -0,0 +1,40 @@ +package com.onarandombox.MultiverseCore.utils.message; + +import co.aikar.commands.ACFUtil; +import co.aikar.commands.CommandIssuer; +import co.aikar.locales.MessageKey; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +final class LocalizedMessage extends Message implements MessageKeyProvider { + + private final @NotNull MessageKeyProvider messageKeyProvider; + + LocalizedMessage( + @NotNull MessageKeyProvider messageKeyProvider, + @NotNull String message, + @NotNull MessageReplacement... replacements + ) { + super(message, replacements); + this.messageKeyProvider = messageKeyProvider; + } + + @Override + public MessageKey getMessageKey() { + return messageKeyProvider.getMessageKey(); + } + + @Override + public @NotNull String formatted(@NotNull PluginLocales locales, @Nullable CommandIssuer commandIssuer) { + Objects.requireNonNull(locales, "locales must not be null"); + + if (getReplacements().length == 0) { + return raw(); + } + return ACFUtil.replaceStrings(locales.getMessage(commandIssuer, getMessageKey()), getReplacements()); + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java new file mode 100644 index 00000000..76821066 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/Message.java @@ -0,0 +1,140 @@ +package com.onarandombox.MultiverseCore.utils.message; + +import co.aikar.commands.ACFUtil; +import co.aikar.commands.CommandIssuer; +import co.aikar.locales.MessageKeyProvider; +import com.onarandombox.MultiverseCore.commandtools.PluginLocales; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +/** + * A message that can be formatted with replacements and localized. + */ +public sealed class Message permits LocalizedMessage { + + /** + * Creates a basic non-localized Message with the given message and replacements. + * + * @param message The message + * @param replacements The replacements + * @return A new Message + */ + @Contract(value = "_, _ -> new", pure = true) + public static Message of(@NotNull String message, @NotNull MessageReplacement... replacements) { + Objects.requireNonNull(message, "message must not be null"); + for (MessageReplacement replacement : replacements) { + Objects.requireNonNull(replacement, "replacements must not contain null"); + } + + return new Message(message, replacements); + } + + /** + * Creates a localized Message with the given message key provider, non-localized message and replacements. + *
+ * The non-localized message is required for conditions where it is not practical to provide a localized message. + *
+ * This message will extend {@link MessageKeyProvider} and delegate to the given message key provider. + * + * @param messageKeyProvider The message key provider + * @param nonLocalizedMessage The non-localized message + * @param replacements The replacements + * @return A new localizable Message + */ + @Contract(value = "_, _, _ -> new", pure = true) + public static Message of( + @NotNull MessageKeyProvider messageKeyProvider, + @NotNull String nonLocalizedMessage, + @NotNull MessageReplacement... replacements + ) { + Objects.requireNonNull(messageKeyProvider, "messageKeyProvider must not be null"); + Objects.requireNonNull(nonLocalizedMessage, "message must not be null"); + for (MessageReplacement replacement : replacements) { + Objects.requireNonNull(replacement, "replacements must not contain null"); + } + + return new LocalizedMessage(messageKeyProvider, nonLocalizedMessage, replacements); + } + + private final @NotNull String message; + private final @NotNull String[] replacements; + + protected Message(@NotNull String message, @NotNull MessageReplacement... replacements) { + this.message = message; + this.replacements = toReplacementsArray(replacements); + } + + /** + * Gets the replacements for this message. + *
+ * This array is guaranteed to be of even length and suitable for use with + * {@link ACFUtil#replaceStrings(String, String...)}. + * + * @return The replacements + */ + public @NotNull String[] getReplacements() { + return replacements; + } + + /** + * Gets the raw, non-localized, non-replaced message. + * + * @return The raw message + */ + public @NotNull String raw() { + return message; + } + + /** + * Gets the formatted message. + *
+ * This is the raw, non-localized message with replacements applied. + * + * @return The formatted message + */ + public @NotNull String formatted() { + if (replacements.length == 0) { + return raw(); + } + return ACFUtil.replaceStrings(message, replacements); + } + + /** + * Gets the formatted message from localization data. + *
+ * This is the localized message with replacements applied. The message is localized using the default locale. + * + * @param locales The MultiverseCore locales provider + * @return The formatted, localized message + */ + public @NotNull String formatted(@NotNull PluginLocales locales) { + return formatted(locales, null); + } + + /** + * Gets the formatted message from localization data. + *
+ * This is the localized message with replacements applied. The message is localized using the locale of the given + * command issuer, if not null. + * + * @param locales The MultiverseCore locales provider + * @param commandIssuer The command issuer the message is for, or null for the console (default locale) + * @return The formatted, localized message + */ + public @NotNull String formatted(@NotNull PluginLocales locales, @Nullable CommandIssuer commandIssuer) { + return formatted(); + } + + private static String[] toReplacementsArray(@NotNull MessageReplacement... replacements) { + String[] replacementsArray = new String[replacements.length * 2]; + int i = 0; + for (MessageReplacement replacement : replacements) { + replacementsArray[i++] = replacement.getKey(); + replacementsArray[i++] = replacement.getReplacement(); + } + return replacementsArray; + } +} diff --git a/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java b/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java new file mode 100644 index 00000000..e6301a35 --- /dev/null +++ b/src/main/java/com/onarandombox/MultiverseCore/utils/message/MessageReplacement.java @@ -0,0 +1,68 @@ +package com.onarandombox.MultiverseCore.utils.message; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Captures string replacements for {@link Message}s. + */ +public final class MessageReplacement { + + /** + * Creates a replacement key for the given key string. + * + * @param key The string to replace + * @return A new replacement key which can be used to create a replacement + */ + @Contract(value = "_ -> new", pure = true) + public static MessageReplacement.Key replace(@NotNull String key) { + return new MessageReplacement.Key(key); + } + + public static final class Key { + + private final @NotNull String key; + + private Key(@NotNull String key) { + this.key = key; + } + + /** + * Creates a replacement for this key. + * + * @param replacement The replacement value, if null it will be replaced with a string equal to "null" + * @return A new message replacement + */ + @Contract(value = "_ -> new", pure = true) + public MessageReplacement with(@Nullable Object replacement) { + return new MessageReplacement(key, replacement); + } + } + + private final @NotNull String key; + private final @NotNull String replacement; + + private MessageReplacement(@NotNull String key, @Nullable Object replacement) { + this.key = key; + this.replacement = String.valueOf(replacement); + } + + /** + * Gets the string to be replaced. + * + * @return The key + */ + public @NotNull String getKey() { + return key; + } + + /** + * Gets the replacement value. + * + * @return The replacement + */ + public @NotNull String getReplacement() { + return replacement; + } +} diff --git a/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt b/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt new file mode 100644 index 00000000..bfd223eb --- /dev/null +++ b/src/test/java/org/mvplugins/multiverse/core/commandtools/LocalizationTest.kt @@ -0,0 +1,256 @@ +package org.mvplugins.multiverse.core.commandtools + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.containsSubstring +import com.onarandombox.MultiverseCore.commandtools.MVCommandIssuer +import com.onarandombox.MultiverseCore.commandtools.MVCommandManager +import com.onarandombox.MultiverseCore.commandtools.PluginLocales +import com.onarandombox.MultiverseCore.utils.MVCorei18n +import com.onarandombox.MultiverseCore.utils.message.Message +import com.onarandombox.MultiverseCore.utils.message.MessageReplacement.replace +import org.bukkit.Bukkit +import org.bukkit.command.CommandSender +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mvplugins.multiverse.core.TestWithMockBukkit +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull + +class LocalizationTest : TestWithMockBukkit() { + + private lateinit var locales: PluginLocales + private lateinit var commandManager: MVCommandManager + + @BeforeTest + fun setUpLocale() { + locales = assertNotNull(multiverseCore.getService(PluginLocales::class.java)) + commandManager = assertNotNull(multiverseCore.getService(MVCommandManager::class.java)) + } + + @Nested + @DisplayName("Given a Message with only a non-localized message") + inner class BasicMessage { + + private val messageString = "This is a test message" + private val message = Message.of(messageString) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the same as the original`() { + assertEquals(messageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be the same as the original`() { + assertEquals(messageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales for a CommandIssuer should be the same as the original`() { + assertEquals(messageString, message.formatted(locales, commandManager.consoleCommandIssuer)) + } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + verify(sender).sendMessage("§9§9$messageString") + } + } + } + + @Nested + @DisplayName("Given a Message with a non-localized message and one replacement") + inner class MessageWithOneReplacement { + + private val replacementKey = "{count}" + private val messageString = "This is a test message with $replacementKey replacement" + private val replacedMessageString = messageString.replace(replacementKey, "one") + + private val message = Message.of(messageString, replace(replacementKey).with("one")) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales for a CommandIssuer should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales, commandManager.consoleCommandIssuer)) + } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + verify(sender).sendMessage("§9§9$replacedMessageString") + } + } + } + + @Nested + @DisplayName("Given a Message with a non-localized message and two replacements") + inner class MessageWithTwoReplacements { + + private val replacementKey1 = "{thing1}" + private val replacementKey2 = "{thing2}" + private val messageString = "$replacementKey1 $replacementKey2" + private val replacedMessageString = messageString + .replace(replacementKey1, "one") + .replace(replacementKey2, "two") + + private val message = Message.of( + messageString, + replace(replacementKey1).with("one"), + replace(replacementKey2).with("two"), + ) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales for a CommandIssuer should be the replaced message string`() { + assertEquals(replacedMessageString, message.formatted(locales, commandManager.consoleCommandIssuer)) + } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + verify(sender).sendMessage("§9§9$replacedMessageString") + } + } + } + + @Nested + @DisplayName("Given a Message with a localized message with one replacement") + inner class LocalizedMessage { + + private val replacementKey = "{world}" + private val replacementValue = "World" + private val messageString = "Hello $replacementKey!" + private val replacedMessageString = messageString.replace(replacementKey, replacementValue) + + private val message = MVCorei18n.CLONE_SUCCESS + .bundle(messageString, replace(replacementKey).with(replacementValue)) + + @Test + fun `The raw message should be the same as the original`() { + assertEquals(messageString, message.raw()) + } + + @Test + fun `The formatted message should be the replaced original string`() { + assertEquals(replacedMessageString, message.formatted()) + } + + @Test + fun `The formatted message with PluginLocales should be different from the replaced original string`() { + assertNotEquals(replacedMessageString, message.formatted(locales)) + } + + @Test + fun `The formatted message with PluginLocales should have performed replacement`() { + assertThat(message.formatted(locales), !containsSubstring(replacementKey)) + assertThat(message.formatted(locales), containsSubstring(replacementValue)) + } + + @Nested + @DisplayName("And a command sender is provided") + inner class WithCommandSender { + + private lateinit var sender: CommandSender + private lateinit var issuer: MVCommandIssuer + + @BeforeTest + fun setUp() { + sender = spy(Bukkit.getConsoleSender()) + issuer = commandManager.getCommandIssuer(sender) + } + + @Test + fun `Sending the issuer the message should send the formatted message to the sender`() { + issuer.sendInfo(message); + + val sentMessage = argumentCaptor { + verify(sender).sendMessage(capture()) + }.firstValue + + assertNotEquals(replacedMessageString, sentMessage) + assertThat(sentMessage, !containsSubstring(replacementKey)) + assertThat(sentMessage, containsSubstring(replacementValue)) + } + } + } +} diff --git a/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt b/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt index e5603af0..ca2b1ae4 100644 --- a/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/inject/InjectionTest.kt @@ -9,6 +9,7 @@ import com.onarandombox.MultiverseCore.api.MVWorldManager import com.onarandombox.MultiverseCore.api.SafeTTeleporter import com.onarandombox.MultiverseCore.commandtools.MVCommandManager import com.onarandombox.MultiverseCore.commandtools.MultiverseCommand +import com.onarandombox.MultiverseCore.commandtools.PluginLocales import com.onarandombox.MultiverseCore.config.MVCoreConfig import com.onarandombox.MultiverseCore.economy.MVEconomist import com.onarandombox.MultiverseCore.listeners.MVChatListener @@ -135,4 +136,9 @@ class InjectionTest : TestWithMockBukkit() { // Also making sure this is not loaded automatically since it's supposed to be disabled during tests assertNull(multiverseCore.getService(MetricsConfigurator::class.java)) } + + @Test + fun `PluginLocales is available as a service`() { + assertNotNull(multiverseCore.getService(PluginLocales::class.java)) + } }