From a658f0f272c25d50ce3c572c388e07895735bd48 Mon Sep 17 00:00:00 2001 From: hannibal2 <24389977+hannibal002@users.noreply.github.com> Date: Mon, 26 Aug 2024 02:46:10 +0200 Subject: Improvement: Multiple Languages (#2403) Co-authored-by: hannibal2 <24389977+hannibal00212@users.noreply.github.com> --- .../skyhanni/config/ConfigUpdaterMigrator.kt | 2 +- .../hannibal2/skyhanni/config/commands/Commands.kt | 2 +- .../skyhanni/config/features/chat/ChatConfig.java | 12 +- .../config/features/chat/TranslatorConfig.java | 37 ++++ .../hannibal2/skyhanni/features/chat/Translator.kt | 155 --------------- .../chat/translation/TranslatableLanguage.kt | 45 +++++ .../features/chat/translation/Translator.kt | 208 +++++++++++++++++++++ 7 files changed, 295 insertions(+), 166 deletions(-) create mode 100644 src/main/java/at/hannibal2/skyhanni/config/features/chat/TranslatorConfig.java delete mode 100644 src/main/java/at/hannibal2/skyhanni/features/chat/Translator.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/chat/translation/TranslatableLanguage.kt create mode 100644 src/main/java/at/hannibal2/skyhanni/features/chat/translation/Translator.kt diff --git a/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt b/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt index 25cab54fb..6d045d56a 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/ConfigUpdaterMigrator.kt @@ -12,7 +12,7 @@ import com.google.gson.JsonPrimitive object ConfigUpdaterMigrator { val logger = LorenzLogger("ConfigMigration") - const val CONFIG_VERSION = 54 + const val CONFIG_VERSION = 55 fun JsonElement.at(chain: List, init: Boolean): JsonElement? { if (chain.isEmpty()) return this if (this !is JsonObject) return null diff --git a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt index 50accf32c..fbd93a4d8 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt +++ b/src/main/java/at/hannibal2/skyhanni/config/commands/Commands.kt @@ -14,7 +14,7 @@ import at.hannibal2.skyhanni.data.TitleManager import at.hannibal2.skyhanni.data.bazaar.HypixelBazaarFetcher import at.hannibal2.skyhanni.features.bingo.card.BingoCardDisplay import at.hannibal2.skyhanni.features.bingo.card.nextstephelper.BingoNextStepHelper -import at.hannibal2.skyhanni.features.chat.Translator +import at.hannibal2.skyhanni.features.chat.translation.Translator import at.hannibal2.skyhanni.features.combat.endernodetracker.EnderNodeTracker import at.hannibal2.skyhanni.features.combat.ghostcounter.GhostUtil import at.hannibal2.skyhanni.features.commands.HelpCommand diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/chat/ChatConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/chat/ChatConfig.java index f9e64f1e0..6a1d129a2 100644 --- a/src/main/java/at/hannibal2/skyhanni/config/features/chat/ChatConfig.java +++ b/src/main/java/at/hannibal2/skyhanni/config/features/chat/ChatConfig.java @@ -3,6 +3,7 @@ package at.hannibal2.skyhanni.config.features.chat; import at.hannibal2.skyhanni.config.FeatureToggle; import com.google.gson.annotations.Expose; import io.github.notenoughupdates.moulconfig.annotations.Accordion; +import io.github.notenoughupdates.moulconfig.annotations.Category; import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDraggableList; import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorKeybind; @@ -104,16 +105,9 @@ public class ChatConfig { @FeatureToggle public boolean hideSacksChange = false; + @Category(name = "Translator", desc = "Chat translator settings.") @Expose - @ConfigOption( - name = "Translator", - desc = "Click on a message to translate it to English.\n" + - "Use §e/shcopytranslation§7 to translate from English.\n" + - "§cTranslation is not guaranteed to be 100% accurate." - ) - @ConfigEditorBoolean - @FeatureToggle - public boolean translator = false; + public TranslatorConfig translator = new TranslatorConfig(); @Expose @ConfigOption(name = "SkyBlock XP in Chat", desc = "Send the SkyBlock XP messages into the chat.") diff --git a/src/main/java/at/hannibal2/skyhanni/config/features/chat/TranslatorConfig.java b/src/main/java/at/hannibal2/skyhanni/config/features/chat/TranslatorConfig.java new file mode 100644 index 000000000..648afbd87 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/config/features/chat/TranslatorConfig.java @@ -0,0 +1,37 @@ +package at.hannibal2.skyhanni.config.features.chat; + +import at.hannibal2.skyhanni.config.FeatureToggle; +import at.hannibal2.skyhanni.features.chat.translation.TranslatableLanguage; +import com.google.gson.annotations.Expose; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorBoolean; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorDropdown; +import io.github.notenoughupdates.moulconfig.annotations.ConfigEditorText; +import io.github.notenoughupdates.moulconfig.annotations.ConfigOption; +import io.github.notenoughupdates.moulconfig.observer.Property; + +public class TranslatorConfig { + + @Expose + @ConfigOption( + name = "Translate On Click", + desc = "Click on a message to translate it to English.\n" + + "Use §e/shcopytranslation§7 to translate from English.\n" + + "§cTranslation is not guaranteed to be 100% accurate." + ) + @ConfigEditorBoolean + @FeatureToggle + public boolean translateOnClick = false; + + @ConfigOption(name = "Language Name", desc = "The name of the language selected below. Note that languages marked as unknown might still be supported.") + @Expose + @ConfigEditorDropdown + public Property languageName = Property.of(TranslatableLanguage.ENGLISH); + + @Expose + @ConfigOption( + name = "Language Code", + desc = "Enter a language code here to translate on chat click into another language. " + + "E.g. `es` for spanish or 'de' for german. Empty for english.") + @ConfigEditorText + public Property languageCode = Property.of("en"); +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/chat/Translator.kt b/src/main/java/at/hannibal2/skyhanni/features/chat/Translator.kt deleted file mode 100644 index 2d3f3aa85..000000000 --- a/src/main/java/at/hannibal2/skyhanni/features/chat/Translator.kt +++ /dev/null @@ -1,155 +0,0 @@ -package at.hannibal2.skyhanni.features.chat - -import at.hannibal2.skyhanni.SkyHanniMod -import at.hannibal2.skyhanni.SkyHanniMod.Companion.coroutineScope -import at.hannibal2.skyhanni.events.LorenzChatEvent -import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule -import at.hannibal2.skyhanni.utils.APIUtil -import at.hannibal2.skyhanni.utils.ChatUtils -import at.hannibal2.skyhanni.utils.ConditionalUtils.transformIf -import at.hannibal2.skyhanni.utils.OSUtils -import at.hannibal2.skyhanni.utils.StringUtils.getPlayerNameFromChatMessage -import at.hannibal2.skyhanni.utils.StringUtils.removeColor -import com.google.gson.JsonArray -import kotlinx.coroutines.launch -import net.minecraft.event.ClickEvent -import net.minecraft.event.HoverEvent -import net.minecraft.util.ChatComponentText -import net.minecraft.util.ChatStyle -import net.minecraftforge.fml.common.eventhandler.EventPriority -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent -import java.net.URLDecoder -import java.net.URLEncoder - -// TODO split into two classes: TranslatorCommand and GoogleTranslator. only communicates via getTranslationFromEnglish and getTranslationToEnglish -@SkyHanniModule -object Translator { - - private val messageContentRegex = Regex(".*: (.*)") - - // Logic for listening for a user click on a chat message is from NotEnoughUpdates - - @SubscribeEvent(priority = EventPriority.LOWEST) - fun onChat(event: LorenzChatEvent) { - if (!isEnabled()) return - - val message = event.message - // TODO use PlayerAllChatEvent and other player chat events - if (message.getPlayerNameFromChatMessage() == null) return - - val editedComponent = event.chatComponent.transformIf({ siblings.isNotEmpty() }) { siblings.last() } - if (editedComponent.chatStyle?.chatClickEvent?.action == ClickEvent.Action.OPEN_URL) return - - val clickStyle = createClickStyle(message, editedComponent.chatStyle) - editedComponent.setChatStyle(clickStyle) - } - - private fun createClickStyle(message: String, style: ChatStyle): ChatStyle { - val text = messageContentRegex.find(message)!!.groupValues[1].removeColor() - style.setChatClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/shtranslate $text")) - style.setChatHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, ChatComponentText("§bClick to translate!"))) - return style - } - - private val config get() = SkyHanniMod.feature.chat - - /* - * Simplified version of the JSON response: - * [ - * [ - * [ - * 'translated sentence one with a space after the punctuation. ' - * 'original sentence one without a space after the punctuation.' - * ], - * [ - * 'translated sentence two without punctuation bc it's last' - * 'original sentence two without punctuation' - * ] - * ], - * null, - * '"target language as a two-letter code following ISO 639-1"', - * ] - */ - - private fun getJSONResponse(urlString: String) = - APIUtil.getJSONResponseAsElement(urlString, false, "Google Translate API") - - private fun getTranslationToEnglish(message: String): String { - val url = - "https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=" + - URLEncoder.encode(message, "UTF-8") - - var messageToSend = "" - val layer1 = getJSONResponse(url).asJsonArray - if (layer1.size() <= 2) return "Error!" - - val language = layer1[2].toString() - if (language == "\"en\"") return "Unable to translate!" - if (language.length != 4) return "Error!" - - val layer2 = try { - layer1[0] as JsonArray - } catch (_: Exception) { - return "Error!" - } - - for (layer3 in layer2) { - val arrayLayer3 = layer3 as? JsonArray ?: continue - val sentence = arrayLayer3[0].toString() - val sentenceWithoutQuotes = sentence.substring(1, sentence.length - 1) - messageToSend = "$messageToSend$sentenceWithoutQuotes" - } - messageToSend = "$messageToSend §7(Language: $language)" - - return URLDecoder.decode(messageToSend, "UTF-8").replace("\\", "") - } - - private fun getTranslationFromEnglish(message: String, lang: String): String { - val url = - "https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=$lang&dt=t&q=" + - URLEncoder.encode(message, "UTF-8") - - val layer1 = getJSONResponse(url).asJsonArray - if (layer1.size() < 1) return "Error!" - val layer2 = layer1[0] as? JsonArray - - val firstSentence = (layer2?.get(0) as? JsonArray)?.get(0).toString() - var messageToSend = firstSentence.substring(0, firstSentence.length - 1) - if (layer2 != null) { - for (sentenceIndex in 1..) { - val message = args.joinToString(" ").removeColor() - - coroutineScope.launch { - val translation = getTranslationToEnglish(message) - if (translation == "Unable to translate!") ChatUtils.userError("Unable to translate message :( (is it in English?)") - else ChatUtils.chat("Found translation: §f$translation") - } - } - - fun fromEnglish(args: Array) { - if (args.size < 2 || args[0].length != 2) { // args[0] is the language code - ChatUtils.userError("Usage: /shcopytranslation ") - return - } - val language = args[0] - val message = args.drop(1).joinToString(" ") - - coroutineScope.launch { - val translation = getTranslationFromEnglish(message, language) - ChatUtils.chat("Copied translation to clipboard: §f$translation") - OSUtils.copyToClipboard(translation) - } - } - - fun isEnabled() = config.translator -} diff --git a/src/main/java/at/hannibal2/skyhanni/features/chat/translation/TranslatableLanguage.kt b/src/main/java/at/hannibal2/skyhanni/features/chat/translation/TranslatableLanguage.kt new file mode 100644 index 000000000..13a19a1dc --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/chat/translation/TranslatableLanguage.kt @@ -0,0 +1,45 @@ +package at.hannibal2.skyhanni.features.chat.translation + +enum class TranslatableLanguage(private val englishName: String, private val nativeName: String, val languageCode: String) { + + // 1. First Language - The primary language of the application. + ENGLISH("English", "", "en"), + + // 2. Well Supported - Major languages commonly used in Europe and North America. + SPANISH("Spanish", "Español", "es"), // Major language in Spain and Latin America + GERMAN("German", "Deutsch", "de"), // Important language in Germany, Austria, and Switzerland + FRENCH("French", "Français", "fr"), // Significant language in France, Canada, and parts of Africa + DUTCH("Dutch", "Nederlands", "nl"), // Spoken in the Netherlands and Belgium + RUSSIAN("Russian", "Русский", "ru"), // Major language in Russia and other parts of Eastern Europe and Central Asia + POLISH("Polish", "Polski", "pl"), // Spoken primarily in Poland + ITALIAN("Italian", "Italiano", "it"), // Important language in Italy and parts of Switzerland + UKRAINIAN("Ukrainian", "Українська", "uk"), // Spoken in Ukraine + PORTUGUESE("Portuguese", "Português", "pt"), // Spoken in Portugal and Brazil + TURKISH("Turkish", "Türkçe", "tr"), // Significant in Turkey and Central Asia + SWEDISH("Swedish", "Svenska", "sv"), // Relevant in Northern Europe + + // 3. Global Languages - Widely spoken languages with significant global presence. + CHINESE("Chinese", "中文", "zh"), // Major language in China and other parts of East Asia + ARABIC("Arabic", "العربية", "ar"), // Significant language in the Middle East and North Africa + JAPANESE("Japanese", "日本語", "ja"), // Major language in Japan + HINDI("Hindi", "हिन्दी", "hi"), // Major language in India + BENGALI("Bengali", "বাংলা", "bn"), // Widely spoken in India and Bangladesh + KOREAN("Korean", "한국어", "ko"), // Important for East Asia + VIETNAMESE("Vietnamese", "Tiếng Việt", "vi"), // Major language in Vietnam + INDONESIAN("Indonesian", "Bahasa Indonesia", "id"), // Key language in Southeast Asia + THAI("Thai", "ภาษาไทย", "th"), // Important in Thailand + + // 4. Other Supported Languages + PERSIAN("Persian", "فارسی", "fa"), // Spoken in Iran and other parts of the Middle East + TAGALOG("Tagalog", "Tagalog", "tl"), // Major language in the Philippines + PUNJABI("Punjabi", "ਪੰਜਾਬੀ", "pa"), // Significant in India and Pakistan + + // 5. need better name + UNKNOWN("Unknown Language", "", ""), + ; + + // Limit to 20 characters so that the text is not too small in the config + private val displayName: String = if (nativeName.isBlank()) englishName else "$englishName/$nativeName".take(20) + + override fun toString(): String = displayName +} diff --git a/src/main/java/at/hannibal2/skyhanni/features/chat/translation/Translator.kt b/src/main/java/at/hannibal2/skyhanni/features/chat/translation/Translator.kt new file mode 100644 index 000000000..389b57fb0 --- /dev/null +++ b/src/main/java/at/hannibal2/skyhanni/features/chat/translation/Translator.kt @@ -0,0 +1,208 @@ +package at.hannibal2.skyhanni.features.chat.translation + +import at.hannibal2.skyhanni.SkyHanniMod +import at.hannibal2.skyhanni.SkyHanniMod.Companion.coroutineScope +import at.hannibal2.skyhanni.config.ConfigUpdaterMigrator +import at.hannibal2.skyhanni.events.ConfigLoadEvent +import at.hannibal2.skyhanni.events.LorenzChatEvent +import at.hannibal2.skyhanni.skyhannimodule.SkyHanniModule +import at.hannibal2.skyhanni.utils.APIUtil +import at.hannibal2.skyhanni.utils.ChatUtils +import at.hannibal2.skyhanni.utils.ConditionalUtils.onToggle +import at.hannibal2.skyhanni.utils.ConditionalUtils.transformIf +import at.hannibal2.skyhanni.utils.OSUtils +import at.hannibal2.skyhanni.utils.SimpleTimeMark +import at.hannibal2.skyhanni.utils.StringUtils.getPlayerNameFromChatMessage +import at.hannibal2.skyhanni.utils.StringUtils.removeColor +import com.google.gson.JsonArray +import kotlinx.coroutines.launch +import net.minecraft.event.ClickEvent +import net.minecraft.event.HoverEvent +import net.minecraft.util.ChatComponentText +import net.minecraft.util.ChatStyle +import net.minecraftforge.fml.common.eventhandler.EventPriority +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent +import java.net.URLDecoder +import java.net.URLEncoder +import kotlin.time.Duration.Companion.milliseconds + +// TODO split into two classes: TranslatorCommand and GoogleTranslator. only communicates via getTranslationFromEnglish and getTranslationToEnglish +@SkyHanniModule +object Translator { + + private val messageContentRegex = Regex(".*: (.*)") + + // Logic for listening for a user click on a chat message is from NotEnoughUpdates + + @SubscribeEvent(priority = EventPriority.LOWEST) + fun onChat(event: LorenzChatEvent) { + if (!isEnabled()) return + + val message = event.message + // TODO use PlayerAllChatEvent and other player chat events + if (message.getPlayerNameFromChatMessage() == null) return + + val editedComponent = event.chatComponent.transformIf({ siblings.isNotEmpty() }) { siblings.last() } + if (editedComponent.chatStyle?.chatClickEvent?.action == ClickEvent.Action.OPEN_URL) return + + val clickStyle = createClickStyle(message, editedComponent.chatStyle) + editedComponent.setChatStyle(clickStyle) + } + + @SubscribeEvent + fun onConfigFix(event: ConfigUpdaterMigrator.ConfigFixEvent) { + event.move(55, "chat.translator", "chat.translator.translateOnClick") + } + + var lastUserChange = SimpleTimeMark.farPast() + + @SubscribeEvent + fun onConfigReload(event: ConfigLoadEvent) { + config.languageCode.onToggle { + if (lastUserChange.passedSince() < 50.milliseconds) return@onToggle + lastUserChange = SimpleTimeMark.now() + + val text = config.languageCode.get() + if (text.isEmpty()) { + config.languageName.set(TranslatableLanguage.ENGLISH) + } else { + for (language in TranslatableLanguage.values()) { + if (language.languageCode.equals(text, ignoreCase = true)) { + config.languageName.set(language) + return@onToggle + } + } + config.languageName.set(TranslatableLanguage.UNKNOWN) + } + } + + config.languageName.onToggle { + if (lastUserChange.passedSince() < 50.milliseconds) return@onToggle + lastUserChange = SimpleTimeMark.now() + + config.languageCode.set(config.languageName.get().languageCode) + } + } + + private fun createClickStyle(message: String, style: ChatStyle): ChatStyle { + val text = messageContentRegex.find(message)!!.groupValues[1].removeColor() + style.setChatClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/shtranslate $text")) + style.setChatHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, ChatComponentText("§bClick to translate!"))) + return style + } + + private val config get() = SkyHanniMod.feature.chat.translator + + /* + * Simplified version of the JSON response: + * [ + * [ + * [ + * 'translated sentence one with a space after the punctuation. ' + * 'original sentence one without a space after the punctuation.' + * ], + * [ + * 'translated sentence two without punctuation bc it's last' + * 'original sentence two without punctuation' + * ] + * ], + * null, + * '"target language as a two-letter code following ISO 639-1"', + * ] + */ + + private fun getJSONResponse(urlString: String) = + APIUtil.getJSONResponseAsElement(urlString, false, "Google Translate API") + + private fun getTranslationToEnglish(message: String): String { + val url = + "https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=" + + URLEncoder.encode(message, "UTF-8") + + var messageToSend = "" + val layer1 = getJSONResponse(url).asJsonArray + if (layer1.size() <= 2) return "Error!" + + val language = layer1[2].toString() + if (language == "\"en\"") return "Unable to translate!" + if (language.length != 4) return "Error!" + + val layer2 = try { + layer1[0] as JsonArray + } catch (_: Exception) { + return "Error!" + } + + for (layer3 in layer2) { + val arrayLayer3 = layer3 as? JsonArray ?: continue + val sentence = arrayLayer3[0].toString() + val sentenceWithoutQuotes = sentence.substring(1, sentence.length - 1) + messageToSend = "$messageToSend$sentenceWithoutQuotes" + } + messageToSend = "$messageToSend §7(Language: $language)" + + return URLDecoder.decode(messageToSend, "UTF-8").replace("\\", "") + } + + private fun getTranslationFromEnglish(message: String, lang: String): String { + val url = + "https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=$lang&dt=t&q=" + + URLEncoder.encode(message, "UTF-8") + + val layer1 = getJSONResponse(url).asJsonArray + if (layer1.size() < 1) return "Error!" + val layer2 = layer1[0] as? JsonArray + + val firstSentence = (layer2?.get(0) as? JsonArray)?.get(0).toString() + var messageToSend = firstSentence.substring(0, firstSentence.length - 1) + if (layer2 != null) { + for (sentenceIndex in 1..) { + val message = args.joinToString(" ").removeColor() + + coroutineScope.launch { + var lang = config.languageCode.get() + val translation = if (lang.isEmpty()) { + getTranslationToEnglish(message) + } else { + getTranslationFromEnglish(message, lang) + } + if (message == translation) { + ChatUtils.userError("Translation is the same as the original message!") + return@launch + } + + if (translation == "Unable to translate!") { + ChatUtils.userError("Unable to translate message :( (is it in English?)") + return@launch + } + ChatUtils.chat("Found translation: §f$translation") + } + } + + fun fromEnglish(args: Array) { + if (args.size < 2 || args[0].length != 2) { // args[0] is the language code + ChatUtils.userError("Usage: /shcopytranslation ") + return + } + val language = args[0] + val message = args.drop(1).joinToString(" ") + + coroutineScope.launch { + val translation = getTranslationFromEnglish(message, language) + ChatUtils.chat("Copied translation to clipboard: §f$translation") + OSUtils.copyToClipboard(translation) + } + } + + fun isEnabled() = config.translateOnClick +} -- cgit