Compare commits

...

13 Commits

Author SHA1 Message Date
Johannes Frohnmeyer c54b646488
chore: update to 1.20.5
ci/woodpecker/push/jfmod Pipeline was successful Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2024-04-25 12:21:59 +02:00
Johannes Frohnmeyer 7c44209d07
chore: update dependencies
ci/woodpecker/push/jfmod Pipeline was successful Details
2024-04-05 15:35:05 +02:00
Johannes Frohnmeyer 3c31d78ab8
fix: catch exceptions during translation service initialization 2024-04-05 15:34:51 +02:00
Johannes Frohnmeyer e1be8aeed9
feat: add keybind for quickly toggling the mod if FAPI is present 2024-02-12 09:19:42 +01:00
Johannes Frohnmeyer e1d19df4c1
chore: update to 1.20.4
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-12-07 19:50:50 +01:00
Johannes Frohnmeyer 49bdf4b921
fix: do not send trailing/preceding whitespace to translate service (based on impl in Translater)
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-11-20 14:39:21 +01:00
Johannes Frohnmeyer 88d44b6737
chore: bump to 1.20.2
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-09-22 20:07:32 +02:00
Johannes Frohnmeyer e04cfaaf93
fix: support disabling Advanced.async on server
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-07-28 14:57:14 +02:00
Johannes Frohnmeyer fa546b3f26
feat: use async on client
ci/woodpecker/push/jfmod Pipeline was successful Details
2023-07-18 23:25:17 +02:00
Johannes Frohnmeyer 21c0cd1000
feat: use async on server 2023-07-18 21:33:41 +02:00
Johannes Frohnmeyer 08d9c0633b
feat: use categories in config 2023-07-18 21:13:28 +02:00
Johannes Frohnmeyer b85cd1ee94
build: use jfMod DSL 2023-07-18 20:47:33 +02:00
Johannes Frohnmeyer 22bd6701d8
Bump to 1.20
ci/woodpecker/push/jfmod Pipeline was successful Details
ci/woodpecker/tag/jfmod Pipeline was successful Details
2023-06-09 18:13:12 +02:00
14 changed files with 493 additions and 263 deletions

View File

@ -1,17 +1,43 @@
import io.gitlab.jfronny.scripts.*
plugins {
id("jfmod") version "1.3-SNAPSHOT"
id("jf.taskgraph") version "1.3-SNAPSHOT"
id("jfmod") version "1.6-SNAPSHOT"
}
allprojects { group = "io.gitlab.jfronny" }
base.archivesName = "google-chat"
jfMod {
minecraftVersion = "1.20.5"
yarn("build.1")
loaderVersion = "0.15.10"
libJfVersion = "3.15.3"
fabricApiVersion = "0.97.6+1.20.5"
modrinth {
projectId = "google-chat"
requiredDependencies.add("libjf")
optionalDependencies.add("modmenu")
}
curseforge {
projectId = "574331"
requiredDependencies.add("libjf")
optionalDependencies.add("modmenu")
}
}
dependencies {
modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v1:${prop("libjf_version")}")
modImplementation("io.gitlab.jfronny.libjf:libjf-translate-v1:${prop("libjf_version")}")
include(modImplementation(fabricApi.module("fabric-message-api-v1", prop("fabric_version")))!!)
modImplementation("io.gitlab.jfronny.libjf:libjf-config-core-v2")
modImplementation("io.gitlab.jfronny.libjf:libjf-translate-v1")
include(modImplementation("net.fabricmc.fabric-api:fabric-message-api-v1")!!)
// Keybind
modCompileOnly("net.fabricmc.fabric-api:fabric-key-binding-api-v1")
modCompileOnly("net.fabricmc.fabric-api:fabric-lifecycle-events-v1")
// Dev env
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-ui-tiny-v1:${prop("libjf_version")}")
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-devutil:${prop("libjf_version")}")
modLocalRuntime("com.terraformersmc:modmenu:6.2.2")
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-config-ui-tiny")
modLocalRuntime("io.gitlab.jfronny.libjf:libjf-devutil")
modLocalRuntime("net.fabricmc.fabric-api:fabric-resource-loader-v0")
modLocalRuntime("com.terraformersmc:modmenu:10.0.0-beta.1")
// for modmenu
modLocalRuntime("net.fabricmc.fabric-api:fabric-screen-api-v1")
modLocalRuntime("net.fabricmc.fabric-api:fabric-key-binding-api-v1")
}

View File

@ -1,17 +0,0 @@
# https://fabricmc.net/develop/
minecraft_version=1.19.4
yarn_mappings=build.2
loader_version=0.14.19
maven_group=io.gitlab.jfronny
archives_base_name=google-chat
modrinth_id=google-chat
modrinth_required_dependencies=libjf
modrinth_optional_dependencies=modmenu
curseforge_id=574331
curseforge_required_dependencies=libjf
curseforge_optional_dependencies=modmenu
libjf_version=3.7.2
fabric_version=0.81.1+1.19.4

View File

@ -0,0 +1,41 @@
package io.gitlab.jfronny.googlechat.client;
import io.gitlab.jfronny.googlechat.GoogleChat;
import io.gitlab.jfronny.googlechat.GoogleChatConfig;
import io.gitlab.jfronny.libjf.config.api.v2.ConfigHolder;
import io.gitlab.jfronny.libjf.config.api.v2.ConfigInstance;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.option.KeyBinding;
import net.minecraft.client.util.InputUtil;
import org.jetbrains.annotations.NotNull;
public class GoogleChatClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ConfigInstance ci = ConfigHolder.getInstance().get(GoogleChat.MOD_ID);
if (ci != null
&& FabricLoader.getInstance().isModLoaded("fabric-key-binding-api-v1")
&& FabricLoader.getInstance().isModLoaded("fabric-lifecycle-events-v1")) {
setupKeybind(ci);
}
}
private static void setupKeybind(@NotNull ConfigInstance ci) {
// Factored out to prevent loading classes if mods are not present
KeyBinding keyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
"key." + GoogleChat.MOD_ID + ".toggle",
InputUtil.Type.KEYSYM,
-1,
KeyBinding.MULTIPLAYER_CATEGORY
));
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (keyBinding.wasPressed()) {
GoogleChatConfig.General.enabled = !GoogleChatConfig.General.enabled;
ci.write();
}
});
}
}

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.googlechat.client.mixin;
import io.gitlab.jfronny.googlechat.GoogleChatCache;
import io.gitlab.jfronny.googlechat.GoogleChat;
import io.gitlab.jfronny.googlechat.TranslationDirection;
import net.minecraft.client.gui.screen.ChatScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -8,9 +9,9 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(ChatScreen.class)
public class ChatScreenMixin {
@ModifyVariable(method = "sendMessage(Ljava/lang/String;Z)Z", at = @At(value = "HEAD"), argsOnly = true, ordinal = 0)
@ModifyVariable(method = "sendMessage(Ljava/lang/String;Z)V", at = @At(value = "HEAD"), argsOnly = true, ordinal = 0)
String googlechat$translateChatText(String chatText) {
if (chatText.startsWith("/")) return chatText; // Bypass for client-side commands (Carpet, ...)
return GoogleChatCache.c2s(chatText);
return GoogleChat.translateIfNeeded(chatText, TranslationDirection.C2S, true);
}
}

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.googlechat.client.mixin;
import io.gitlab.jfronny.googlechat.GoogleChatCache;
import io.gitlab.jfronny.googlechat.GoogleChat;
import io.gitlab.jfronny.googlechat.TranslationDirection;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
@ -11,6 +12,6 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable;
public class ClientPlayerEntityMixin {
@ModifyVariable(method = "sendMessage(Lnet/minecraft/text/Text;)V", at = @At("HEAD"), argsOnly = true, ordinal = 0)
Text googlechat$translateMessage(Text source) {
return GoogleChatCache.c2s(source);
return GoogleChat.translateIfNeeded(source, TranslationDirection.C2S, true);
}
}

View File

@ -1,40 +1,100 @@
package io.gitlab.jfronny.googlechat.client.mixin;
import com.mojang.authlib.GameProfile;
import io.gitlab.jfronny.googlechat.GoogleChatCache;
import io.gitlab.jfronny.googlechat.*;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.hud.*;
import net.minecraft.client.network.message.MessageHandler;
import net.minecraft.network.message.MessageType;
import net.minecraft.network.message.SignedMessage;
import net.minecraft.client.util.NarratorManager;
import net.minecraft.network.message.*;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.*;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;
@Mixin(MessageHandler.class)
public class MessageHandlerMixin {
public abstract class MessageHandlerMixin {
@Shadow @Final private MinecraftClient client;
@Shadow protected abstract void narrate(MessageType.Parameters params, Text message);
@Redirect(method = "onChatMessage(Lnet/minecraft/network/message/SignedMessage;Lcom/mojang/authlib/GameProfile;Lnet/minecraft/network/message/MessageType$Parameters;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/message/MessageType$Parameters;applyChatDecoration(Lnet/minecraft/text/Text;)Lnet/minecraft/text/Text;"))
Text googlechat$applyDecoration(MessageType.Parameters instance, Text content, SignedMessage args$signed, GameProfile args$sender) {
return instance.applyChatDecoration(googlechat$shouldTranslate(args$sender) ? GoogleChatCache.s2c(content) : content);
@Unique CompletableFuture<Void> googlechat$currentFuture = CompletableFuture.completedFuture(null);
@Unique ThreadLocal<GameProfile> sender = new ThreadLocal<>();
@Redirect(method = "onGameMessage(Lnet/minecraft/text/Text;Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;setOverlayMessage(Lnet/minecraft/text/Text;Z)V"))
private void googlechat$onGameMessage$setOverlayMessage(InGameHud instance, Text message, boolean tinted) {
if (googlechat$shouldTranslate()) googlechat$schedule(() -> instance.setOverlayMessage(googlechat$s2c(message), tinted));
else instance.setOverlayMessage(message, tinted);
}
@Redirect(method = "processChatMessageInternal(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/text/Text;Lcom/mojang/authlib/GameProfile;ZLjava/time/Instant;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/message/MessageType$Parameters;applyChatDecoration(Lnet/minecraft/text/Text;)Lnet/minecraft/text/Text;"))
Text googlechat$applyDecoration2(MessageType.Parameters instance, Text content, MessageType.Parameters args$params, SignedMessage args$message, Text args$decorated, GameProfile args$sender) {
return instance.applyChatDecoration(googlechat$shouldTranslate(args$sender) ? GoogleChatCache.s2c(content) : content);
@Redirect(method = "onGameMessage(Lnet/minecraft/text/Text;Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;addMessage(Lnet/minecraft/text/Text;)V"))
private void googlechat$onGameMessage$addMessage(ChatHud instance, Text text) {
if (googlechat$shouldTranslate()) googlechat$schedule(() -> instance.addMessage(googlechat$s2c(text)));
else instance.addMessage(text);
}
@ModifyVariable(method = "onProfilelessMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageType$Parameters;)V", at = @At(value = "HEAD"), argsOnly = true, ordinal = 0)
Text googlechat$applyTranslation(Text origin) {
return GoogleChatCache.s2c(origin);
@Redirect(method = "onGameMessage(Lnet/minecraft/text/Text;Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/NarratorManager;narrateSystemMessage(Lnet/minecraft/text/Text;)V"))
private void googlechat$onGameMessage$narrateSystemMessage(NarratorManager instance, Text text) {
if (googlechat$shouldTranslate()) googlechat$schedule(() -> instance.narrateSystemMessage(googlechat$s2c(text)));
else instance.narrateSystemMessage(text);
}
@ModifyVariable(method = "onGameMessage(Lnet/minecraft/text/Text;Z)V", at = @At(value = "HEAD"), argsOnly = true, ordinal = 0)
Text googlechat$applyTranslation2(Text origin) {
return GoogleChatCache.s2c(origin);
@Redirect(method = "method_45745(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/text/Text;Ljava/time/Instant;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;addMessage(Lnet/minecraft/text/Text;)V"))
private void googlechat$onProfilelessMessage$addMessage(ChatHud instance, Text text) {
if (googlechat$shouldTranslate()) googlechat$schedule(() -> instance.addMessage(googlechat$s2c(text)));
else instance.addMessage(text);
}
private boolean googlechat$shouldTranslate(GameProfile sender) {
return client != null && client.player != null && sender != null && !sender.getId().equals(client.player.getUuid());
@Redirect(method = "method_45745(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/text/Text;Ljava/time/Instant;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/message/MessageHandler;narrate(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/text/Text;)V"))
private void googlechat$onProfilelessMessage$narrate(MessageHandler instance, MessageType.Parameters params, Text message) {
if (instance != (Object) this) GoogleChat.LOGGER.warn("Mismatched message handler in onGameMessage");
if (googlechat$shouldTranslate()) googlechat$schedule(() -> narrate(params, googlechat$s2c(message)));
else narrate(params, message);
}
@Inject(method = "onChatMessage(Lnet/minecraft/network/message/SignedMessage;Lcom/mojang/authlib/GameProfile;Lnet/minecraft/network/message/MessageType$Parameters;)V", at = @At("HEAD"))
private void googlechat$onChatMessage$extractSender(SignedMessage message, GameProfile sender, MessageType.Parameters params, CallbackInfo ci) {
this.sender.set(sender);
}
@Redirect(method = "processChatMessageInternal(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/text/Text;Lcom/mojang/authlib/GameProfile;ZLjava/time/Instant;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/ChatHud;addMessage(Lnet/minecraft/text/Text;Lnet/minecraft/network/message/MessageSignatureData;Lnet/minecraft/client/gui/hud/MessageIndicator;)V"))
private void googlechat$processChatMessageInternal$addMessage(ChatHud instance, Text message, MessageSignatureData signature, MessageIndicator indicator) {
if (googlechat$shouldTranslate()) googlechat$schedule(() -> instance.addMessage(googlechat$s2c(message), signature, indicator));
else instance.addMessage(message, signature, indicator);
}
@Redirect(method = "processChatMessageInternal(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/text/Text;Lcom/mojang/authlib/GameProfile;ZLjava/time/Instant;)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/message/MessageHandler;narrate(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/text/Text;)V"))
private void googlechat$processChatMessageInternal$narrate(MessageHandler instance, MessageType.Parameters params, Text message) {
if (instance != (Object) this) GoogleChat.LOGGER.warn("Mismatched message handler in onGameMessage");
if (googlechat$shouldTranslate()) googlechat$schedule(() -> narrate(params, googlechat$s2c(message)));
else narrate(params, message);
}
@Inject(method = "processChatMessageInternal(Lnet/minecraft/network/message/MessageType$Parameters;Lnet/minecraft/network/message/SignedMessage;Lnet/minecraft/text/Text;Lcom/mojang/authlib/GameProfile;ZLjava/time/Instant;)Z", at = @At("RETURN"))
private void googlechat$processChatMessageInternal$clearSender(MessageType.Parameters params, SignedMessage message, Text decorated, GameProfile sender, boolean onlyShowSecureChat, Instant receptionTimestamp, CallbackInfoReturnable<Boolean> cir) {
this.sender.remove();
}
private Text googlechat$s2c(Text message) {
return GoogleChat.translateIfNeeded(message, TranslationDirection.S2C, true);
}
private void googlechat$schedule(Runnable runnable) {
if (!GoogleChatConfig.Advanced.async) runnable.run();
else googlechat$currentFuture.whenCompleteAsync((_1, _2) -> runnable.run()).exceptionally(throwable -> {
GoogleChat.LOGGER.error("Something went wrong while processing a message", throwable);
return null;
});
}
private boolean googlechat$shouldTranslate() {
if (!GoogleChatConfig.General.enabled) return false;
if (client == null || client.player == null) return false;
var sender = this.sender.get();
if (sender == null) return true;
return !sender.getId().equals(client.player.getUuid());
}
}

View File

@ -1,6 +1,9 @@
package io.gitlab.jfronny.googlechat;
import io.gitlab.jfronny.commons.log.Logger;
import io.gitlab.jfronny.commons.io.cache.FixedSizeMap;
import io.gitlab.jfronny.commons.logger.SystemLoggerPlus;
import io.gitlab.jfronny.commons.throwable.Coerce;
import io.gitlab.jfronny.commons.throwable.Try;
import io.gitlab.jfronny.libjf.translate.api.Language;
import io.gitlab.jfronny.libjf.translate.api.TranslateService;
import net.fabricmc.api.EnvType;
@ -8,42 +11,76 @@ import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.text.*;
import java.util.Arrays;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GoogleChat implements ModInitializer {
public static final String MOD_ID = "google-chat";
public static final Logger LOGGER = Logger.forName(MOD_ID);
public static final SystemLoggerPlus LOGGER = SystemLoggerPlus.forName(MOD_ID);
public static TranslateService<?> TRANSLATE_SERVICE;
private static final boolean IS_SERVER = FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER;
private static final Map<Text, Text> s2ct = new FixedSizeMap<>(GoogleChatConfig.Advanced.cacheSize);
private static final Map<Text, Text> c2st = new FixedSizeMap<>(GoogleChatConfig.Advanced.cacheSize);
private static final Map<TextContent, TextContent> s2cc = new FixedSizeMap<>(GoogleChatConfig.Advanced.cacheSize);
private static final Map<TextContent, TextContent> c2sc = new FixedSizeMap<>(GoogleChatConfig.Advanced.cacheSize);
private static final Map<String, String> s2cs = new FixedSizeMap<>(GoogleChatConfig.Advanced.cacheSize);
private static final Map<String, String> c2ss = new FixedSizeMap<>(GoogleChatConfig.Advanced.cacheSize);
@Override
public void onInitialize() {
ForkJoinPool.commonPool().execute(() -> TRANSLATE_SERVICE = TranslateService.getConfigured());
ForkJoinPool.commonPool().execute(Try.handle(Coerce.runnable(() -> TRANSLATE_SERVICE = TranslateService.getConfigured()), e -> LOGGER.error("Could not initialize translation service", e)));
}
public static Text translateIfNeeded(Text source, Direction direction, boolean respectRegex) {
if (shouldSkipOutright(direction)) return source;
public static void onConfigChange() {
synchronized (s2ct) {
s2ct.clear();
}
synchronized (c2st) {
c2st.clear();
}
synchronized (s2cc) {
s2cc.clear();
}
synchronized (c2sc) {
c2sc.clear();
}
synchronized (s2cs) {
s2cs.clear();
}
synchronized (c2ss) {
c2ss.clear();
}
}
public static Text translateIfNeeded(Text source, TranslationDirection direction, boolean respectRegex) {
if (source == null) return null;
if (direction.shouldSkipOutright()) return source;
String sourceString = toString(source);
if (respectRegex && failsRegex(sourceString, direction)) return source;
MutableText translated;
if (GoogleChatConfig.desugar) {
translated = Text.literal(translateIfNeeded(sourceString, direction, true));
} else {
translated = MutableText.of(translateIfNeeded(source.getContent(), direction, false))
.setStyle(source.getStyle());
for (Text sibling : source.getSiblings()) {
translated.append(translateIfNeeded(sibling, direction, false));
if (respectRegex && direction.failsRegex(sourceString)) return source;
return computeIfAbsent2(direction == TranslationDirection.C2S ? c2st : s2ct, source, t -> {
MutableText translated;
if (GoogleChatConfig.Processing.desugar) {
translated = Text.literal(translateIfNeeded(sourceString, direction, true));
} else {
translated = MutableText.of(translateIfNeeded(t.getContent(), direction, false))
.setStyle(t.getStyle());
for (Text sibling : t.getSiblings()) {
translated.append(translateIfNeeded(sibling, direction, false));
}
}
}
if (GoogleChatConfig.debugLogs) LOGGER.info("Translated " + sourceString + " to " + toString(translated));
if (GoogleChatConfig.translationTooltip) {
return source.copy().styled(style -> addHover(style, Text.literal("Translated: ").append(translated)));
} else if (translated.getStyle().getHoverEvent() == null) {
return translated.styled(style -> addHover(style, Text.literal("Original: ").append(source)));
} else {
return translated;
}
if (GoogleChatConfig.Advanced.debugLogs) LOGGER.info("Translated " + sourceString + " to " + toString(translated));
if (GoogleChatConfig.General.translationTooltip) {
return t.copy().styled(style -> addHover(style, Text.literal("Translated: ").append(translated)));
} else if (translated.getStyle().getHoverEvent() == null) {
return translated.styled(style -> addHover(style, Text.literal("Original: ").append(t)));
} else {
return translated;
}
});
}
private static String toString(Text text) {
@ -55,28 +92,31 @@ public class GoogleChat implements ModInitializer {
return sb.toString();
}
public static TextContent translateIfNeeded(TextContent source, Direction direction, boolean respectRegex) {
if (shouldSkipOutright(direction)) return source;
public static TextContent translateIfNeeded(TextContent source, TranslationDirection direction, boolean respectRegex) {
if (source == null || source == PlainTextContent.EMPTY) return source;
if (direction.shouldSkipOutright()) return source;
String sourceString = toString(source);
if (respectRegex && failsRegex(sourceString, direction)) return source;
//TODO This method (and the check for translatable args) should be converted to a switch pattern when available
if (source instanceof TranslatableTextContent tx) {
Object[] args = tx.getArgs();
args = Arrays.copyOf(args, args.length);
// We're not translating TranslatableText, but are translating arguments
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Text tx1) args[i] = translateIfNeeded(tx1, direction, false);
else if (args[i] instanceof TextContent tx1) args[i] = translateIfNeeded(tx1, direction, false);
else if (args[i] instanceof String tx1) args[i] = translateIfNeeded(tx1, direction, false);
else args[i] = args[i];
if (respectRegex && direction.failsRegex(sourceString)) return source;
return computeIfAbsent2(direction == TranslationDirection.C2S ? c2sc : s2cc, source, t -> {
//TODO This method (and the check for translatable args) should be converted to a switch pattern when available
if (t instanceof TranslatableTextContent tx) {
Object[] args = tx.getArgs();
args = Arrays.copyOf(args, args.length);
// We're not translating TranslatableText, but are translating arguments
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Text tx1) args[i] = translateIfNeeded(tx1, direction, false);
else if (args[i] instanceof TextContent tx1) args[i] = translateIfNeeded(tx1, direction, false);
else if (args[i] instanceof String tx1) args[i] = translateIfNeeded(tx1, direction, false);
else args[i] = args[i];
}
return new TranslatableTextContent(tx.getKey(), translateIfNeeded(tx.getFallback(), direction, false), args);
} else if (t instanceof PlainTextContent.Literal tx) {
return new PlainTextContent.Literal(translateIfNeeded(tx.string(), direction, false));
} else {
// LOGGER.info("Unhandled text type: " + source.getClass() + " (" + source + ")");
return t;
}
return new TranslatableTextContent(tx.getKey(), translateIfNeeded(tx.getFallback(), direction, false), args);
} else if (source instanceof LiteralTextContent tx) {
return new LiteralTextContent(translateIfNeeded(tx.string(), direction, false));
} else {
// LOGGER.info("Unhandled text type: " + source.getClass() + " (" + source + ")");
return source;
}
});
}
private static String toString(TextContent text) {
@ -92,53 +132,36 @@ public class GoogleChat implements ModInitializer {
return style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverText));
}
public static String translateIfNeeded(String source, Direction direction, boolean respectRegex) {
if (shouldSkipOutright(direction)) return source;
if (respectRegex && failsRegex(source, direction)) return source;
try {
// Ignore generics since this is apparently not something java supports
@SuppressWarnings("rawtypes") TranslateService svc = GoogleChat.TRANSLATE_SERVICE;
if (svc == null) throw new NullPointerException("Translate service uninitialized");
Language sourceLang = svc.parseLang(direction.source());
Language targetLang = svc.parseLang(direction.target());
//noinspection unchecked
return svc.translate(source, sourceLang, targetLang);
} catch (Throwable e) {
LOGGER.error("Could not translate text: " + source, e);
return source;
}
private static final Pattern SURROUNDING_SPACE_PATTERN = Pattern.compile("^(\\s*)(.*\\S+)(\\s*)$", Pattern.MULTILINE);
public static String translateIfNeeded(String source, TranslationDirection direction, boolean respectRegex) {
if (source == null) return null;
if (direction.shouldSkipOutright()) return source;
if (respectRegex && direction.failsRegex(source)) return source;
return computeIfAbsent2(direction == TranslationDirection.C2S ? c2ss : s2cs, source, t -> {
try {
Matcher m = SURROUNDING_SPACE_PATTERN.matcher(source);
if (!m.find()) return source;
// Ignore generics since this is apparently not something java supports
@SuppressWarnings("rawtypes") TranslateService svc = GoogleChat.TRANSLATE_SERVICE;
if (svc == null) throw new NullPointerException("Translate service uninitialized");
Language sourceLang = svc.parseLang(direction.source());
Language targetLang = svc.parseLang(direction.target());
//noinspection unchecked
return m.group(1) + svc.translate(m.group(2), sourceLang, targetLang) + m.group(3);
} catch (Throwable e) {
LOGGER.error("Could not translate text: " + source, e);
return source;
}
});
}
private static boolean failsRegex(String text, Direction direction) {
boolean isSender = (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) == (direction == Direction.C2S);
if (isSender) return text.matches(GoogleChatConfig.sendingRegex) == GoogleChatConfig.sendingRegexIsBlacklist;
else return text.matches(GoogleChatConfig.receivingRegex) == GoogleChatConfig.receivingRegexIsBlacklist;
}
private static boolean shouldSkipOutright(Direction direction) {
return !GoogleChatConfig.enabled || !hasTarget(direction);
}
public static boolean hasTarget(Direction direction) {
return !direction.target().equals("auto");
}
public enum Direction {
C2S,
S2C;
public String source() {
return switch (this) {
case C2S -> GoogleChatConfig.clientLanguage;
case S2C -> GoogleChatConfig.serverLanguage;
};
}
public String target() {
return switch (this) {
case C2S -> GoogleChatConfig.serverLanguage;
case S2C -> GoogleChatConfig.clientLanguage;
};
private static <K, V> V computeIfAbsent2(Map<K, V> map, K key, Function<K, V> compute) {
if (!GoogleChatConfig.Advanced.async && !IS_SERVER) return map.computeIfAbsent(key, compute);
synchronized (map) {
if (map.containsKey(key)) return map.get(key);
V value = compute.apply(key);
map.put(key, value);
return value;
}
}
}

View File

@ -1,41 +0,0 @@
package io.gitlab.jfronny.googlechat;
import io.gitlab.jfronny.commons.cache.FixedSizeMap;
import net.minecraft.text.Text;
import java.util.Map;
public class GoogleChatCache {
private static Map<Text, Text> s2ct = new FixedSizeMap<>(GoogleChatConfig.cacheSize);
private static Map<Text, Text> c2st = new FixedSizeMap<>(GoogleChatConfig.cacheSize);
private static Map<String, String> s2cs = new FixedSizeMap<>(GoogleChatConfig.cacheSize);
private static Map<String, String> c2ss = new FixedSizeMap<>(GoogleChatConfig.cacheSize);
public static void clear() {
s2ct.clear();
c2st.clear();
s2cs.clear();
c2ss.clear();
}
public static void onConfigChange() {
GoogleChat.LOGGER.info("Config Change");
clear();
}
public static Text s2c(Text msg) {
return s2ct.computeIfAbsent(msg, m -> GoogleChat.translateIfNeeded(m, GoogleChat.Direction.S2C, true));
}
public static Text c2s(Text msg) {
return c2st.computeIfAbsent(msg, m -> GoogleChat.translateIfNeeded(m, GoogleChat.Direction.C2S, true));
}
public static String s2c(String msg) {
return s2cs.computeIfAbsent(msg, m -> GoogleChat.translateIfNeeded(m, GoogleChat.Direction.S2C, true));
}
public static String c2s(String msg) {
return c2ss.computeIfAbsent(msg, m -> GoogleChat.translateIfNeeded(m, GoogleChat.Direction.C2S, true));
}
}

View File

@ -1,60 +1,99 @@
package io.gitlab.jfronny.googlechat;
import io.gitlab.jfronny.commons.serialize.gson.api.v1.Ignore;
import io.gitlab.jfronny.libjf.config.api.v1.*;
import io.gitlab.jfronny.commons.serialize.SerializeReader;
import io.gitlab.jfronny.commons.serialize.annotations.Ignore;
import io.gitlab.jfronny.commons.throwable.Assume;
import io.gitlab.jfronny.commons.throwable.ThrowingConsumer;
import io.gitlab.jfronny.libjf.config.api.v2.*;
import io.gitlab.jfronny.libjf.config.api.v2.dsl.ConfigBuilder;
import io.gitlab.jfronny.libjf.config.api.v2.dsl.Migration;
import net.fabricmc.api.*;
import net.fabricmc.loader.api.*;
@JfConfig(referencedConfigs = "libjf-translate-v1")
public class GoogleChatConfig {
@Entry public static boolean enabled = true;
@Entry public static String serverLanguage = "auto";
@Entry public static String clientLanguage = "en";
@Entry public static boolean translationTooltip = false;
@Entry public static boolean desugar = false;
@Entry public static String receivingRegex = "";
@Entry public static boolean receivingRegexIsBlacklist = true;
@Entry public static String sendingRegex = "";
@Entry public static boolean sendingRegexIsBlacklist = true;
@Entry(min = 1, max = 1024) public static int cacheSize = 256;
@Entry public static boolean debugLogs = FabricLoader.getInstance().isDevelopmentEnvironment();
import java.util.function.Consumer;
@Preset
public static void client() {
enabled = true;
if (!serverLanguage.equals("auto")) {
serverLanguage = "auto";
clientLanguage = "en";
String tmp = receivingRegex;
receivingRegex = sendingRegex;
sendingRegex = tmp;
@JfConfig(tweaker = GoogleChatConfig.class)
public class GoogleChatConfig {
@Category(referencedConfigs = "libjf-translate-v1")
public static class General {
@Entry public static boolean enabled = true;
@Entry public static String serverLanguage = "auto";
@Entry public static String clientLanguage = "en";
@Entry public static boolean translationTooltip = false;
@Preset
public static void client() {
enabled = true;
if (!serverLanguage.equals("auto")) {
serverLanguage = "auto";
clientLanguage = "en";
}
}
@Preset
public static void server() {
enabled = true;
if (!clientLanguage.equals("auto")) {
clientLanguage = "auto";
serverLanguage = "en";
}
}
}
@Preset
public static void server() {
enabled = true;
if (!clientLanguage.equals("auto")) {
clientLanguage = "auto";
serverLanguage = "en";
String tmp = receivingRegex;
receivingRegex = sendingRegex;
sendingRegex = tmp;
}
@Category
public static class Processing {
@Entry public static boolean desugar = false;
@Entry public static String receivingRegex = "";
@Entry public static boolean receivingRegexIsBlacklist = true;
@Entry public static String sendingRegex = "";
@Entry public static boolean sendingRegexIsBlacklist = true;
}
@Category
public static class Advanced {
@Entry(min = 1, max = 1024) public static int cacheSize = 256;
@Entry public static boolean async = true;
@Entry public static boolean debugLogs = FabricLoader.getInstance().isDevelopmentEnvironment();
}
@Ignore private static boolean initial = true;
@Verifier
public static void verify() {
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER && !clientLanguage.equals("auto")) {
if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER && !General.clientLanguage.equals("auto")) {
System.err.println("""
Your client language is not set to "auto" and you are using a server.
This setup is not recommended! Please set up GoogleChat according to its documentation!""");
}
if (!initial) GoogleChatCache.onConfigChange();
if (!initial) GoogleChat.onConfigChange();
initial = false;
}
public static ConfigBuilder<?> tweak(ConfigBuilder<?> builder) {
return builder
.addMigration("enabled", of(reader -> General.enabled = reader.nextBoolean()))
.addMigration("serverLanguage", of(reader -> General.serverLanguage = reader.nextString()))
.addMigration("clientLanguage", of(reader -> General.clientLanguage = reader.nextString()))
.addMigration("translationTooltip", of(reader -> General.translationTooltip = reader.nextBoolean()))
.addMigration("desugar", of(reader -> Processing.desugar = reader.nextBoolean()))
.addMigration("receivingRegex", of(reader -> Processing.receivingRegex = reader.nextString()))
.addMigration("receivingRegexIsBlacklist", of(reader -> Processing.receivingRegexIsBlacklist = reader.nextBoolean()))
.addMigration("sendingRegex", of(reader -> Processing.sendingRegex = reader.nextString()))
.addMigration("sendingRegexIsBlacklist", of(reader -> Processing.sendingRegexIsBlacklist = reader.nextBoolean()))
.addMigration("cacheSize", of(reader -> Advanced.cacheSize = reader.nextInt()))
.addMigration("debugLogs", of(reader -> Advanced.debugLogs = reader.nextBoolean()));
}
private static Migration of(ThrowingConsumer<SerializeReader<?, ?>, Exception> dm) {
//TODO use the libjf-native method
Consumer<SerializeReader<?, ?>> cn = ((ThrowingConsumer<SerializeReader<?, ?>, RuntimeException>) (ThrowingConsumer) dm)::accept;
return new Migration() {
@Override
public <TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> void apply(Reader reader) throws TEx {
cn.accept(reader);
}
};
}
static {
JFC_GoogleChatConfig.ensureInitialized();
}

View File

@ -0,0 +1,38 @@
package io.gitlab.jfronny.googlechat;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.FabricLoader;
public enum TranslationDirection {
C2S,
S2C;
public String source() {
return switch (this) {
case C2S -> GoogleChatConfig.General.clientLanguage;
case S2C -> GoogleChatConfig.General.serverLanguage;
};
}
public String target() {
return switch (this) {
case C2S -> GoogleChatConfig.General.serverLanguage;
case S2C -> GoogleChatConfig.General.clientLanguage;
};
}
public boolean hasTarget() {
return !target().equals("auto");
}
public boolean shouldSkipOutright() {
if (!GoogleChatConfig.General.enabled) return true;
return !hasTarget();
}
public boolean failsRegex(String text) {
boolean isSender = (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT) == (this == TranslationDirection.C2S);
if (isSender) return text.matches(GoogleChatConfig.Processing.sendingRegex) == GoogleChatConfig.Processing.sendingRegexIsBlacklist;
else return text.matches(GoogleChatConfig.Processing.receivingRegex) == GoogleChatConfig.Processing.receivingRegexIsBlacklist;
}
}

View File

@ -4,37 +4,56 @@ import io.gitlab.jfronny.googlechat.*;
import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.message.v1.ServerMessageDecoratorEvent;
import net.minecraft.network.message.MessageDecorator;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static io.gitlab.jfronny.googlechat.GoogleChat.hasTarget;
import static io.gitlab.jfronny.libjf.LibJf.LOGGER;
public class GoogleChatServer implements DedicatedServerModInitializer {
public class GoogleChatServer implements DedicatedServerModInitializer, MessageDecorator {
@Override
public void onInitializeServer() {
// Default phase is executed between CONTENT and STYLING
// Perform translation there instead of during CONTENT to better support other mods (such as chat-transform)
// If this causes an incompatibility, I'll add my own phase
ServerMessageDecoratorEvent.EVENT.register(Event.DEFAULT_PHASE, (sender, message) -> {
Text original;
if (sender != null && hasTarget(GoogleChat.Direction.C2S)) {
if (hasTarget(GoogleChat.Direction.S2C)) return CompletableFuture.completedFuture(message); // Do not translate back and forth
ServerMessageDecoratorEvent.EVENT.register(Event.DEFAULT_PHASE, this);
}
@Override
public Text decorate(@Nullable ServerPlayerEntity sender, Text original) {
if (!GoogleChatConfig.General.enabled) return original;
if (!GoogleChatConfig.Advanced.async) {
return decorate(sender, new TranslatableContainer.Sync(original)).text();
}
try {
return decorate(sender, new TranslatableContainer.Async(CompletableFuture.completedFuture(original)))
.text()
.exceptionally(e -> {
GoogleChat.LOGGER.error("Could not compute translation", e);
return original;
})
.get();
} catch (InterruptedException | ExecutionException e) {
GoogleChat.LOGGER.error("Could not synchronize async translation for synchronous decorator", e);
return original;
}
}
private <K, T extends TranslatableContainer<K, T>> T decorate(@Nullable ServerPlayerEntity sender, T original) {
T message = original;
if (sender != null) { // Client messages should first be translated to the server language
if (TranslationDirection.C2S.hasTarget()) {
if (TranslationDirection.S2C.hasTarget()) {
// Do not translate back and forth
return message;
}
}
if (sender != null) { // Client messages should first be translated to the server language
if (hasTarget(GoogleChat.Direction.C2S) && hasTarget(GoogleChat.Direction.S2C)) // Do not translate back and forth
return CompletableFuture.completedFuture(message);
original = message;
message = GoogleChatCache.c2s(message);
if (GoogleChatConfig.debugLogs) LOGGER.info("Applied C2S translation from " + original + " to " + message);
}
// All messages should be translated to the client language before sending
original = message;
message = GoogleChatCache.s2c(message);
if (GoogleChatConfig.debugLogs) LOGGER.info("Applied S2C translation from " + original + " to " + message);
return CompletableFuture.completedFuture(message);
});
message = message.translate(TranslationDirection.C2S);
}
// All messages should be translated to the client language before sending
message = message.translate(TranslationDirection.S2C);
return message;
}
}

View File

@ -0,0 +1,32 @@
package io.gitlab.jfronny.googlechat.server;
import io.gitlab.jfronny.googlechat.*;
import net.minecraft.text.Text;
import java.util.concurrent.CompletableFuture;
import static io.gitlab.jfronny.libjf.LibJf.LOGGER;
public sealed interface TranslatableContainer<T, S extends TranslatableContainer<T, S>> {
S translate(TranslationDirection direction);
record Sync(Text text) implements TranslatableContainer<Text, Sync> {
@Override
public Sync translate(TranslationDirection direction) {
return new Sync(translateAndLog(text, direction));
}
}
record Async(CompletableFuture<Text> text) implements TranslatableContainer<CompletableFuture<Text>, Async> {
@Override
public Async translate(TranslationDirection direction) {
return new Async(text.thenApplyAsync(msg -> translateAndLog(msg, direction)));
}
}
static Text translateAndLog(final Text source, final TranslationDirection direction) {
var translated = GoogleChat.translateIfNeeded(source, direction, true);
if (GoogleChatConfig.Advanced.debugLogs) LOGGER.info("Applied C2S translation from " + source + " to " + translated);
return translated;
}
}

View File

@ -1,28 +1,35 @@
{
"google-chat.jfconfig.title": "GoogleChat",
"google-chat.jfconfig.enabled": "Enabled",
"google-chat.jfconfig.enabled.tooltip": "Whether translations should be applied",
"google-chat.jfconfig.serverLanguage": "Server Language",
"google-chat.jfconfig.serverLanguage.tooltip": "The language of the server used in translations. \"auto\" will disable translating your own messages",
"google-chat.jfconfig.clientLanguage": "Client Language",
"google-chat.jfconfig.clientLanguage.tooltip": "Your own language used in translations. \"auto\" will disable translating messages by other server members",
"google-chat.jfconfig.translationTooltip": "Translation Tooltip",
"google-chat.jfconfig.translationTooltip.tooltip": "Display translations as a tooltip (on hover) and keep the original message. This will overwrite other tooltips",
"google-chat.jfconfig.desugar": "Desugar",
"google-chat.jfconfig.desugar.tooltip": "Translate all messages as plain strings. This will remove formatting but may be more accurate",
"google-chat.jfconfig.receivingRegex": "Receiving Regex",
"google-chat.jfconfig.receivingRegex.tooltip": "A Regex pattern to check whether a received message should be translated",
"google-chat.jfconfig.receivingRegexIsBlacklist": "Receiving Regex Is Blacklist",
"google-chat.jfconfig.receivingRegexIsBlacklist.tooltip": "Whether the relevant regex should blacklist messages from translation instead of whitelisting",
"google-chat.jfconfig.sendingRegex": "Sending Regex",
"google-chat.jfconfig.sendingRegex.tooltip": "A Regex pattern to check whether a sent message should be translated",
"google-chat.jfconfig.sendingRegexIsBlacklist": "Sending Regex Is Blacklist",
"google-chat.jfconfig.sendingRegexIsBlacklist.tooltip": "Whether the relevant regex should blacklist messages from translation instead of whitelisting",
"google-chat.jfconfig.cacheSize": "Cache Size",
"google-chat.jfconfig.cacheSize.tooltips": "The size of each message cache. Since there are four caches, the actual size will be four times this.",
"google-chat.jfconfig.debugLogs": "Debug Logs",
"google-chat.jfconfig.debugLogs.tooltips": "Log additional information about message processing. Useful for debugging",
"google-chat.jfconfig.general.title": "General",
"google-chat.jfconfig.general.enabled": "Enabled",
"google-chat.jfconfig.general.enabled.tooltip": "Whether translations should be applied",
"google-chat.jfconfig.general.serverLanguage": "Server Language",
"google-chat.jfconfig.general.serverLanguage.tooltip": "The language of the server used in translations. \"auto\" will disable translating your own messages",
"google-chat.jfconfig.general.clientLanguage": "Client Language",
"google-chat.jfconfig.general.clientLanguage.tooltip": "Your own language used in translations. \"auto\" will disable translating messages by other server members",
"google-chat.jfconfig.general.translationTooltip": "Translation Tooltip",
"google-chat.jfconfig.general.translationTooltip.tooltip": "Display translations as a tooltip (on hover) and keep the original message. This will overwrite other tooltips",
"google-chat.jfconfig.processing.title": "Processing",
"google-chat.jfconfig.processing.desugar": "Desugar",
"google-chat.jfconfig.processing.desugar.tooltip": "Translate all messages as plain strings. This improves translation quality but removes all message styling",
"google-chat.jfconfig.processing.receivingRegex": "Receiving Regex",
"google-chat.jfconfig.processing.receivingRegex.tooltip": "A Regex pattern to check whether a received message should be translated",
"google-chat.jfconfig.processing.receivingRegexIsBlacklist": "Receiving Regex Is Blacklist",
"google-chat.jfconfig.processing.receivingRegexIsBlacklist.tooltip": "Whether the relevant regex should blacklist messages from translation instead of whitelisting",
"google-chat.jfconfig.processing.sendingRegex": "Sending Regex",
"google-chat.jfconfig.processing.sendingRegex.tooltip": "A Regex pattern to check whether a sent message should be translated",
"google-chat.jfconfig.processing.sendingRegexIsBlacklist": "Sending Regex Is Blacklist",
"google-chat.jfconfig.processing.sendingRegexIsBlacklist.tooltip": "Whether the relevant regex should blacklist messages from translation instead of whitelisting",
"google-chat.jfconfig.advanced.title": "Advanced",
"google-chat.jfconfig.advanced.cacheSize": "Cache Size",
"google-chat.jfconfig.advanced.cacheSize.tooltips": "The size of each message cache. Since there are six caches, the actual size will be six times this.",
"google-chat.jfconfig.advanced.async": "Async",
"google-chat.jfconfig.advanced.async.tooltips": "Whether to asynchronously process messages. Should prevent some stutters, but might cause other issues. Disable if you no longer receive messages",
"google-chat.jfconfig.advanced.debugLogs": "Debug Logs",
"google-chat.jfconfig.advanced.debugLogs.tooltips": "Log additional information about message processing. Useful for debugging",
"google-chat.jfconfig.client": "Client",
"google-chat.jfconfig.server": "Server"
"google-chat.jfconfig.server": "Server",
"key.google-chat.toggle": "Toggle GoogleChat"
}

View File

@ -17,7 +17,8 @@
"entrypoints": {
"libjf:config": ["io.gitlab.jfronny.googlechat.JFC_GoogleChatConfig"],
"server": ["io.gitlab.jfronny.googlechat.server.GoogleChatServer"],
"main": ["io.gitlab.jfronny.googlechat.GoogleChat"]
"main": ["io.gitlab.jfronny.googlechat.GoogleChat"],
"client": ["io.gitlab.jfronny.googlechat.client.GoogleChatClient"]
},
"mixins": [
{
@ -28,7 +29,7 @@
"depends": {
"fabricloader": ">=0.12.12",
"minecraft": "*",
"libjf-config-core-v1": "*",
"libjf-config-core-v2": "*",
"libjf-translate-v1": "*",
"fabric-message-api-v1": "*"
}