diff --git a/src/main/java/com/loki/lochat/LoChat.java b/src/main/java/com/loki/lochat/LoChat.java index d8bc22f..438cc6d 100644 --- a/src/main/java/com/loki/lochat/LoChat.java +++ b/src/main/java/com/loki/lochat/LoChat.java @@ -15,6 +15,7 @@ import com.loki.lochat.gradient.GradientModule; import com.loki.lochat.integrations.DiscordIntegration; import com.loki.lochat.managers.AutoMessageManager; import com.loki.lochat.managers.CustomCommandManager; +import com.loki.lochat.translate.TranslationService; import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; @@ -31,6 +32,7 @@ public final class LoChat extends JavaPlugin { private ServiceRegistry serviceRegistry; private ConfigManager configManager; private MessageConfig messageConfig; + private TranslationService translationService; private GradientModule gradientModule; private AutoMessageManager autoMessageManager; private CustomCommandManager customCommandManager; @@ -53,6 +55,12 @@ public final class LoChat extends JavaPlugin { // Конфиги configManager = initializer.initConfigs(); messageConfig = initializer.initMessageConfig(); + translationService = new TranslationService( + configManager.getTranslationEndpoint(), + configManager.getTranslationApiKey(), + configManager.isTranslationEnabled(), + getLogger() + ); // Service Registry (SOLID DI) serviceRegistry = initializer.initServiceRegistry(configManager, messageConfig); @@ -141,4 +149,8 @@ public final class LoChat extends JavaPlugin { public DiscordIntegration getDiscordIntegration() { return discordIntegration; } + + public TranslationService getTranslationService() { + return translationService; + } } diff --git a/src/main/java/com/loki/lochat/config/ConfigManager.java b/src/main/java/com/loki/lochat/config/ConfigManager.java index b4a456d..b69083b 100644 --- a/src/main/java/com/loki/lochat/config/ConfigManager.java +++ b/src/main/java/com/loki/lochat/config/ConfigManager.java @@ -227,6 +227,20 @@ public class ConfigManager { return accessor.getAnnouncementTitleDuration(); } + // ========== Chat Translation ========== + + public boolean isTranslationEnabled() { + return accessor.isTranslationEnabled(); + } + + public String getTranslationEndpoint() { + return accessor.getTranslationEndpoint(); + } + + public String getTranslationApiKey() { + return accessor.getTranslationApiKey(); + } + // ========== LoPreff integration ========== public boolean isLopreffEnabled() { diff --git a/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java b/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java index 1ca1a6a..96db60e 100644 --- a/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java +++ b/src/main/java/com/loki/lochat/config/core/ConfigAccessor.java @@ -136,6 +136,20 @@ public class ConfigAccessor { return config.getInt("announcements.title-duration", 3); } + // ========== Chat Translation ========== + + public boolean isTranslationEnabled() { + return config.getBoolean("chat.translation.enabled", false); + } + + public String getTranslationEndpoint() { + return config.getString("chat.translation.endpoint", "https://libretranslate.com"); + } + + public String getTranslationApiKey() { + return config.getString("chat.translation.api-key", ""); + } + // ========== LoPreff integration ========== public boolean isLopreffEnabled() { diff --git a/src/main/java/com/loki/lochat/config/core/ConfigLoader.java b/src/main/java/com/loki/lochat/config/core/ConfigLoader.java index 3de7ff1..9a7265e 100644 --- a/src/main/java/com/loki/lochat/config/core/ConfigLoader.java +++ b/src/main/java/com/loki/lochat/config/core/ConfigLoader.java @@ -20,7 +20,7 @@ public class ConfigLoader { public void updateConfig() { int currentVersion = config.getInt("config-version", 1); - int requiredVersion = 5; + int requiredVersion = 6; if (currentVersion < requiredVersion) { plugin.getLogger().info("Обновление конфига с версии " + currentVersion + " до " + requiredVersion); @@ -51,6 +51,9 @@ public class ConfigLoader { setIfMissing("lopreff.use-gradient-name", true); setIfMissing("lopreff.use-custom-prefix", true); setIfMissing("moderation.voice-mute-console-command", ""); + setIfMissing("chat.translation.enabled", false); + setIfMissing("chat.translation.endpoint", "https://libretranslate.com"); + setIfMissing("chat.translation.api-key", ""); } private void setIfMissing(String path, Object value) { diff --git a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java index 513cd55..0b1ca3d 100644 --- a/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java +++ b/src/main/java/com/loki/lochat/renderer/EnhancedChatRenderer.java @@ -3,6 +3,7 @@ package com.loki.lochat.renderer; import com.loki.lochat.renderer.components.EmojiComponent; import com.loki.lochat.renderer.components.GradientBuilder; import com.loki.lochat.renderer.components.UrlProcessor; +import com.loki.lochat.translate.TranslationService; import com.loki.lochat.utils.format.TextFormatter; import com.loki.lochat.utils.player.MentionHandler; @@ -12,10 +13,12 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -52,6 +55,7 @@ public class EnhancedChatRenderer implements ChatRenderer { if (viewer instanceof Player vp) { processed = mentionHandler.processMentions(processed, source, vp); + processed = applyTranslation(processed, vp); } // Получаем формат из конфига @@ -198,6 +202,66 @@ public class EnhancedChatRenderer implements ChatRenderer { .clickEvent(ClickEvent.suggestCommand("/msg " + player.getName() + " ")); } + private Component applyTranslation(Component message, Player viewer) { + if (!cfg.isTranslationEnabled()) { + return message; + } + com.loki.lochat.LoChat loChat = (com.loki.lochat.LoChat) plugin; + TranslationService ts = loChat.getTranslationService(); + if (!ts.isEnabled()) { + return message; + } + String plainText = PlainTextComponentSerializer.plainText().serialize(message); + if (plainText.isBlank()) { + return message; + } + String viewerLang = viewer.locale().getLanguage(); + String translated = ts.translate(plainText, viewerLang); + if (translated.equals(plainText)) { + return message; + } + String langName = LANGUAGE_NAMES.getOrDefault(viewerLang, viewerLang.toUpperCase(java.util.Locale.ROOT)); + Component hover = Component.text() + .append(com.loki.lochat.utils.format.ChatFormatter.parse( + "&#AAAAAA[" + langName + "]&#FFFFFF ")) + .append(Component.text(translated)) + .build(); + return message.hoverEvent(HoverEvent.showText(hover)); + } + + private static final Map LANGUAGE_NAMES = new HashMap<>(); + static { + LANGUAGE_NAMES.put("en", "EN"); + LANGUAGE_NAMES.put("ru", "RU"); + LANGUAGE_NAMES.put("de", "DE"); + LANGUAGE_NAMES.put("fr", "FR"); + LANGUAGE_NAMES.put("es", "ES"); + LANGUAGE_NAMES.put("pt", "PT"); + LANGUAGE_NAMES.put("it", "IT"); + LANGUAGE_NAMES.put("nl", "NL"); + LANGUAGE_NAMES.put("pl", "PL"); + LANGUAGE_NAMES.put("uk", "UA"); + LANGUAGE_NAMES.put("zh", "CN"); + LANGUAGE_NAMES.put("ja", "JP"); + LANGUAGE_NAMES.put("ko", "KR"); + LANGUAGE_NAMES.put("ar", "AR"); + LANGUAGE_NAMES.put("tr", "TR"); + LANGUAGE_NAMES.put("vi", "VN"); + LANGUAGE_NAMES.put("th", "TH"); + LANGUAGE_NAMES.put("cs", "CS"); + LANGUAGE_NAMES.put("sv", "SV"); + LANGUAGE_NAMES.put("da", "DA"); + LANGUAGE_NAMES.put("fi", "FI"); + LANGUAGE_NAMES.put("el", "EL"); + LANGUAGE_NAMES.put("hu", "HU"); + LANGUAGE_NAMES.put("ro", "RO"); + LANGUAGE_NAMES.put("no", "NO"); + LANGUAGE_NAMES.put("sk", "SK"); + LANGUAGE_NAMES.put("bg", "BG"); + LANGUAGE_NAMES.put("hr", "HR"); + LANGUAGE_NAMES.put("sr", "SR"); + } + private TextColor parseColor(String hex) { if (hex == null || hex.isBlank()) { return FALLBACK_COLOR; diff --git a/src/main/java/com/loki/lochat/translate/TranslationService.java b/src/main/java/com/loki/lochat/translate/TranslationService.java new file mode 100644 index 0000000..6d9df24 --- /dev/null +++ b/src/main/java/com/loki/lochat/translate/TranslationService.java @@ -0,0 +1,117 @@ +package com.loki.lochat.translate; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +public class TranslationService { + + private static final Duration TIMEOUT = Duration.ofSeconds(10); + + private final Logger logger; + private final HttpClient httpClient; + private final Gson gson; + private final Map> cache = new ConcurrentHashMap<>(); + private final String endpoint; + private final String apiKey; + private final boolean enabled; + + public TranslationService(String endpoint, String apiKey, boolean enabled, Logger logger) { + this.endpoint = endpoint != null ? endpoint.replaceAll("/+$", "") : ""; + this.apiKey = apiKey; + this.enabled = enabled; + this.logger = logger; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(TIMEOUT) + .build(); + this.gson = new Gson(); + } + + public boolean isEnabled() { + return enabled && !endpoint.isBlank(); + } + + public String translate(String text, String targetLanguage) { + if (!isEnabled() || text == null || text.isBlank()) { + return text; + } + + String langCode = localeToLanguageCode(targetLanguage); + String cacheKey = text.intern(); + + Map langMap = cache.get(cacheKey); + if (langMap != null) { + String cached = langMap.get(langCode); + if (cached != null) { + return cached; + } + } + + try { + String translation = fetchTranslation(text, langCode); + cache.computeIfAbsent(cacheKey, k -> new ConcurrentHashMap<>()).put(langCode, translation); + return translation; + } catch (Exception e) { + logger.warning("Translation failed for \"" + abbreviate(text) + "\" to " + langCode + ": " + e.getMessage()); + return text; + } + } + + private String fetchTranslation(String text, String targetLang) throws Exception { + JsonObject body = new JsonObject(); + body.addProperty("q", text); + body.addProperty("source", "auto"); + body.addProperty("target", targetLang); + body.addProperty("format", "text"); + if (apiKey != null && !apiKey.isBlank()) { + body.addProperty("api_key", apiKey); + } + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(endpoint + "/translate")) + .timeout(TIMEOUT) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("API returned " + response.statusCode() + ": " + response.body()); + } + + JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); + return json.get("translatedText").getAsString(); + } + + public static String localeToLanguageCode(String locale) { + if (locale == null || locale.isBlank()) { + return "en"; + } + String lang = locale.split("_")[0].toLowerCase(Locale.ROOT); + return switch (lang) { + case "zh" -> "zh-CN"; + case "zh-tw", "zh-hk" -> "zh-TW"; + default -> lang; + }; + } + + public void clearCache() { + cache.clear(); + } + + private static String abbreviate(String text) { + return text.length() > 40 ? text.substring(0, 37) + "..." : text; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 7c61714..ede16bb 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -27,6 +27,18 @@ chat: max-message-length: 2000 clear-lines: 100 + # ── Chat Translation ────── + # Translates chat messages on hover into each viewer's Minecraft client language. + # Uses LibreTranslate API (free, open-source). + # Public instances: https://libretranslate.com, https://translate.argosopentech.com + # You can self-host: https://github.com/LibreTranslate/LibreTranslate + translation: + enabled: false + # API endpoint URL (LibreTranslate-compatible) + endpoint: "https://libretranslate.com" + # Optional API key if the endpoint requires authentication + api-key: "" + # ── Chat Appearance ──────────────────────────────────────────────────────────── appearance: