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.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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
if (hex == null || hex.isBlank()) {
|
||||
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
|
||||
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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue