feat: add chat message translation via LibreTranslate
Adds optional translation feature that shows translated chat messages on hover in each viewer's Minecraft client language. - TranslationService with LibreTranslate API support and caching - Config options: enabled, endpoint, api-key - Hover translation on chat messages in EnhancedChatRenderer - Disabled by default, supports any LibreTranslate-compatible endpoint
This commit is contained in:
parent
4737d52cdd
commit
25cd6c3e88
7 changed files with 237 additions and 1 deletions
|
|
@ -15,6 +15,7 @@ import com.loki.lochat.gradient.GradientModule;
|
||||||
import com.loki.lochat.integrations.DiscordIntegration;
|
import com.loki.lochat.integrations.DiscordIntegration;
|
||||||
import com.loki.lochat.managers.AutoMessageManager;
|
import com.loki.lochat.managers.AutoMessageManager;
|
||||||
import com.loki.lochat.managers.CustomCommandManager;
|
import com.loki.lochat.managers.CustomCommandManager;
|
||||||
|
import com.loki.lochat.translate.TranslationService;
|
||||||
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
@ -31,6 +32,7 @@ public final class LoChat extends JavaPlugin {
|
||||||
private ServiceRegistry serviceRegistry;
|
private ServiceRegistry serviceRegistry;
|
||||||
private ConfigManager configManager;
|
private ConfigManager configManager;
|
||||||
private MessageConfig messageConfig;
|
private MessageConfig messageConfig;
|
||||||
|
private TranslationService translationService;
|
||||||
private GradientModule gradientModule;
|
private GradientModule gradientModule;
|
||||||
private AutoMessageManager autoMessageManager;
|
private AutoMessageManager autoMessageManager;
|
||||||
private CustomCommandManager customCommandManager;
|
private CustomCommandManager customCommandManager;
|
||||||
|
|
@ -53,6 +55,12 @@ public final class LoChat extends JavaPlugin {
|
||||||
// Конфиги
|
// Конфиги
|
||||||
configManager = initializer.initConfigs();
|
configManager = initializer.initConfigs();
|
||||||
messageConfig = initializer.initMessageConfig();
|
messageConfig = initializer.initMessageConfig();
|
||||||
|
translationService = new TranslationService(
|
||||||
|
configManager.getTranslationEndpoint(),
|
||||||
|
configManager.getTranslationApiKey(),
|
||||||
|
configManager.isTranslationEnabled(),
|
||||||
|
getLogger()
|
||||||
|
);
|
||||||
|
|
||||||
// Service Registry (SOLID DI)
|
// Service Registry (SOLID DI)
|
||||||
serviceRegistry = initializer.initServiceRegistry(configManager, messageConfig);
|
serviceRegistry = initializer.initServiceRegistry(configManager, messageConfig);
|
||||||
|
|
@ -141,4 +149,8 @@ public final class LoChat extends JavaPlugin {
|
||||||
public DiscordIntegration getDiscordIntegration() {
|
public DiscordIntegration getDiscordIntegration() {
|
||||||
return discordIntegration;
|
return discordIntegration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TranslationService getTranslationService() {
|
||||||
|
return translationService;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,20 @@ public class ConfigManager {
|
||||||
return accessor.getAnnouncementTitleDuration();
|
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 ==========
|
// ========== LoPreff integration ==========
|
||||||
|
|
||||||
public boolean isLopreffEnabled() {
|
public boolean isLopreffEnabled() {
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,20 @@ public class ConfigAccessor {
|
||||||
return config.getInt("announcements.title-duration", 3);
|
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 ==========
|
// ========== LoPreff integration ==========
|
||||||
|
|
||||||
public boolean isLopreffEnabled() {
|
public boolean isLopreffEnabled() {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public class ConfigLoader {
|
||||||
|
|
||||||
public void updateConfig() {
|
public void updateConfig() {
|
||||||
int currentVersion = config.getInt("config-version", 1);
|
int currentVersion = config.getInt("config-version", 1);
|
||||||
int requiredVersion = 5;
|
int requiredVersion = 6;
|
||||||
|
|
||||||
if (currentVersion < requiredVersion) {
|
if (currentVersion < requiredVersion) {
|
||||||
plugin.getLogger().info("Обновление конфига с версии " + currentVersion + " до " + requiredVersion);
|
plugin.getLogger().info("Обновление конфига с версии " + currentVersion + " до " + requiredVersion);
|
||||||
|
|
@ -51,6 +51,9 @@ public class ConfigLoader {
|
||||||
setIfMissing("lopreff.use-gradient-name", true);
|
setIfMissing("lopreff.use-gradient-name", true);
|
||||||
setIfMissing("lopreff.use-custom-prefix", true);
|
setIfMissing("lopreff.use-custom-prefix", true);
|
||||||
setIfMissing("moderation.voice-mute-console-command", "");
|
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) {
|
private void setIfMissing(String path, Object value) {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.loki.lochat.renderer;
|
||||||
import com.loki.lochat.renderer.components.EmojiComponent;
|
import com.loki.lochat.renderer.components.EmojiComponent;
|
||||||
import com.loki.lochat.renderer.components.GradientBuilder;
|
import com.loki.lochat.renderer.components.GradientBuilder;
|
||||||
import com.loki.lochat.renderer.components.UrlProcessor;
|
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.format.TextFormatter;
|
||||||
import com.loki.lochat.utils.player.MentionHandler;
|
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.event.HoverEvent;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import net.kyori.adventure.text.format.TextColor;
|
import net.kyori.adventure.text.format.TextColor;
|
||||||
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
|
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
@ -52,6 +55,7 @@ public class EnhancedChatRenderer implements ChatRenderer {
|
||||||
|
|
||||||
if (viewer instanceof Player vp) {
|
if (viewer instanceof Player vp) {
|
||||||
processed = mentionHandler.processMentions(processed, source, 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() + " "));
|
.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<String, String> 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) {
|
private TextColor parseColor(String hex) {
|
||||||
if (hex == null || hex.isBlank()) {
|
if (hex == null || hex.isBlank()) {
|
||||||
return FALLBACK_COLOR;
|
return FALLBACK_COLOR;
|
||||||
|
|
|
||||||
117
src/main/java/com/loki/lochat/translate/TranslationService.java
Normal file
117
src/main/java/com/loki/lochat/translate/TranslationService.java
Normal file
|
|
@ -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<String, Map<String, String>> 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<String, String> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,18 @@ chat:
|
||||||
max-message-length: 2000
|
max-message-length: 2000
|
||||||
clear-lines: 100
|
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 ────────────────────────────────────────────────────────────
|
# ── Chat Appearance ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
appearance:
|
appearance:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue