Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat Translation 2: Electric Boogaloo #72

Open
wants to merge 20 commits into
base: v7
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions core/assets/bundles/bundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ client.command.builder.description = Starts a build path with optional arguments
client.command.miner.description = Starts a mine path with optional arguments, prioritized based on core item count
client.command.buildmine.description = Buildpath (self) + mine (all).
client.command.!.description = Lets you start messages with an !
client.command.tl.description = Translates a message to another language
client.command.shrug.description = Sends the shrug unicode emoji with an optional message
client.command.login.description = Used for CN. [scarlet]Don't use this if you care at all about security
client.command.marker.description = Adds a marker with specified name at (x, y) or your current position if x and y are not specified
Expand Down Expand Up @@ -598,6 +599,7 @@ command.assist = Assist Player
command.move = Move
openlink = Open Link
copylink = Copy Link
translation = Translation
back = Back
max = Max
objective = Map Objective
Expand Down Expand Up @@ -1351,6 +1353,8 @@ setting.removecorenukes.name = Automatically Remove Reactors Built Within[gray]
setting.chat.category = Messaging & Online
setting.clearchatonleave.name = Clear Message History When Joining A New Game
setting.logmsgstoconsole.name = Log Chat Messages To Console
setting.enabletranslation.name = Enable Message Translation
setting.enabletranslation.description = Translation uses the LibreTranslate API.\nMessages are translated to your Mindustry language, or English if the API doesn't support it.\nOnly messages from players are translated.
setting.clientjoinleave.name = Always Send Join/Leave Messages
setting.clientjoinleave.description = Tries to prevent the server and the client both sending
setting.highlightcryptomsg.name = Highlight Messages From Users Whose Certificates You Have Imported
Expand Down
1 change: 1 addition & 0 deletions core/assets/bundles/bundle_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ command.assist = Assister
command.move = Bouger
openlink = Ouvrir le lien
copylink = Copier le lien
translation = Traduction
back = Retour
max = Max
objective = Objectif de la Carte
Expand Down
1 change: 1 addition & 0 deletions core/assets/bundles/bundle_ja.properties
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ command.assist = プレイヤー補助
command.move = 移動
openlink = リンクを開く
copylink = リンクをコピー
translation = 翻訳
back = 戻る
max = Max
objective = マップの目標
Expand Down
1 change: 1 addition & 0 deletions core/assets/bundles/bundle_ko.properties
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ command.assist = 플레이어 지원
command.move = 이동
openlink = 링크 열기
copylink = 링크 복사
translation = 번역
back = 뒤로가기
max = 최대
objective = 맵 목표
Expand Down
4 changes: 4 additions & 0 deletions core/assets/bundles/bundle_zh_CN.properties
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ client.command.builder.description = 建造模式, 可自选某些参数, 优先
client.command.miner.description = 挖掘模式,可自选某些参数,根据核心内的物品数量确定挖掘优先级
client.command.buildmine.description = 建筑路径 (self) + mine (all).
client.command.!.description = 发送消息时额外发送一个 !
client.command.tl.description = 翻译信息至其他语言
client.command.shrug.description = 发送消息时额外发送带有耸肩Unicode的表情符号
client.command.login.description = [scarlet]如果你在乎你的个人隐私和安全问题, 那就不要使用这个
client.command.marker.description = 在 (x, y) 处添加可指定名称的标记, 若未给出X和Y的位置则在你的当前位置添加标记
Expand Down Expand Up @@ -597,6 +598,7 @@ command.assist = 协助建造
command.move = 移动
openlink = 打开链接
copylink = 复制链接
translation = 翻译
back = 返回
max = 最大值
objective = 任务目标
Expand Down Expand Up @@ -1350,6 +1352,8 @@ setting.removecorenukes.name = 自动拆除在距离核心[gray]20[]格方块内
setting.chat.category = 消息 & 在线
setting.clearchatonleave.name = 加入新游戏时清除历史消息记录
setting.logmsgstoconsole.name = 打印聊天消息到控制台
setting.enabletranslation.name = 启用翻译
setting.enabletranslation.description = 翻译使用LibreTranslate API。\n信息会被翻译成你所选的Mindustry语言,在API不支持所选语言时仅会翻译成英语。\n仅有玩家发送的消息会被翻译。
setting.clientjoinleave.name = 总是发送加入/离开消息
setting.clientjoinleave.description = 会尝试阻止服务器和客户端同时发送
setting.highlightcryptomsg.name = 高亮你导入证书的用户发出的消息
Expand Down
1 change: 1 addition & 0 deletions core/assets/bundles/bundle_zh_TW.properties
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ command.assist = 協助玩家
command.move = 移動
openlink = 開啟連結
copylink = 複製連結
translation = 翻譯
back = 返回
max = 最大量
objective = 地圖目標
Expand Down
18 changes: 16 additions & 2 deletions core/src/mindustry/client/ClientVars.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
import arc.util.Nullable;
import arc.util.*;
import kotlin.*;
import mindustry.content.*;
import mindustry.entities.units.*;
import mindustry.gen.*;
import mindustry.net.*;
import mindustry.ui.*;
import mindustry.world.*;
import mindustry.client.utils.*;
import mindustry.game.EventType.*;
import mindustry.world.blocks.defense.*;
import mindustry.world.blocks.distribution.*;
import org.jetbrains.annotations.*;
Expand Down Expand Up @@ -72,4 +72,18 @@ public class ClientVars {
@NotNull public static String lastCertName = "";
public static boolean isBuildingLock; // Whether the building state is being controlled by networking
public static int pluginVersion; // Version of the foo plugin that is found on the server

// Translating
public static String targetLang; // Language to translate messages to
public static Seq<String> supportedLangs = new Seq<>(); // List of supported languages
public static boolean enableTranslation = Core.settings.getBool("enabletranslation", true);
@NotNull public static Color translatedColor = Color.sky;

static {
Events.on(ClientLoadEvent.class, e -> Translating.languages(langs -> {
supportedLangs = langs;
if (!supportedLangs.contains(targetLang = Locale.getDefault().getLanguage().substring(0, 2)))
targetLang = "en";
}));
}
}
12 changes: 12 additions & 0 deletions core/src/mindustry/client/Commands.kt
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,18 @@ fun setup() {
sendMessage("!" + if (args.size == 1) args[0] else "")
}

register("tl [lang] [message...]", Core.bundle.get("client.command.tl.description")) { args, _ ->
if (args.isEmpty()) return@register
val msg = if (args.size == 1) args[0]
else if (supportedLangs.contains(args[0])) args[1]
else args[0] + " " + args[1]
val lang = if (args.size == 2 && supportedLangs.contains(args[0])) args[0] else "en"

Translating.translate(msg, lang) { translation ->
sendMessage("$translation [gray](translated)[]")
}
}

register("shrug [message...]", Core.bundle.get("client.command.shrug.description")) { args, _ ->
sendMessage("¯\\_(ツ)_/¯ " + if (args.size == 1) args[0] else "")
}
Expand Down
163 changes: 163 additions & 0 deletions core/src/mindustry/client/utils/Translating.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package mindustry.client.utils;

import arc.func.*;
import arc.struct.*;
import arc.util.*;
import arc.util.Http.*;
import arc.util.serialization.JsonWriter.*;
import mindustry.client.*;
import mindustry.io.*;

import java.io.*;

/** Partial wrapper for the <a href=https://libretranslate.com>LibreTranslate API</a>
* <!-- Is this how I'm supposed to write async --->
* @author Weathercold
*/
public class Translating {
/** List of mirrors can be found <a href=https://github.com/LibreTranslate/LibreTranslate#mirrors>here</a>.
* If you see a mirror not working, please make a pr.
*/
public static volatile ObjectMap<String, Boolean> servers = ObjectMap.of(
//"libretranslate.com", false, requires API key :(
// Thanks to Allen for hosting this instance and Nautilus for giving permission to use it
// https://github.com/TomtheCoder2/mainPlugin/blob/master/src/main/java/mindustry/plugin/minimods/Translate.java#L219
"http://168.119.234.142:5000", false,
"https://translate.argosopentech.com", false,
"https://translate.terraprint.co", false,
"https://lt.vern.cc", false,
"https://libretranslate.de", false,
"https://translate.api.skitzen.com", false,
"https://translate.fortytwo-it.com", false
);

// Might break certain mods idk
static {JsonIO.json.setOutputType(OutputType.json);}


/** Retrieve an array of supported languages.
* @param success The callback to run if no errors occurred.
*/
public static void languages(Cons<Seq<String>> success) {
fetch(
"/languages",
res -> {
StringMap[] langs = JsonIO.json.fromJson(StringMap[].class, res);
Seq<String> codes = new Seq<>(langs.length);
for (StringMap lang : langs) codes.add(lang.get("code"));
success.get(codes);
}
);
}

/** Get the language of the specified text.
* @param success The callback to run if no errors occurred.
*/
public static void detect(String text, Cons<String> success) {
if (text == null) {
Log.err(new NullPointerException("Detect text cannot be null."));
return;
}

fetch(
"/detect",
StringMap.of("q", text),
res -> success.get(JsonIO.json.fromJson(StringMap[].class, res)[0].get("language"))
);
}

/** Detect source then translate */
public static void translate(String text, String target, Cons<String> success) {
translate(text, "auto", target, success);
}

/** Translate the specified text from the source language to the target language.
* @param source Source language code.
* @param target target language code.
* @param success The callback to run if no errors occurred.
*/
public static void translate(String text, String source, String target, Cons<String> success) {
if (text == null || source == null || target == null) {
Log.err(new NullPointerException("Translate arguments cannot be null."));
return;
}
if (source.equals(target)) {success.get(text); return;}

fetch(
"/translate",
StringMap.of(
"q", text,
"source", source,
"target", target
),
res -> {
String translation = JsonIO.json.fromJson(StringMap.class, res).get("translatedText");
if (translation.length() <= 256)
success.get(translation);
else
Log.warn("Translation is too long (@chars)", translation.length());
}
);
}

private static void fetch(String api, Cons<String> success) {
fetch(api, HttpMethod.GET, null, success);
}
private static void fetch(String api, @Nullable StringMap body, Cons<String> success) {
fetch(api, HttpMethod.POST, body, success);
}
private static void fetch(String api, HttpMethod method, @Nullable StringMap body, Cons<String> success) {
String server = servers.findKey(false, false); // find available server
if (server == null) {
Log.warn("Rate limit reached on all servers. Aborting translation.");
return;
}

Http.post(server + api)
.method(method)
.header("Content-Type", "application/json")
.content(JsonIO.json.toJson(body, StringMap.class, String.class))
.error(e -> {
if (e instanceof HttpStatusException hse) {
switch (hse.status) {
case BAD_REQUEST -> Log.warn("Bad request, aborting translation.");
case INTERNAL_SERVER_ERROR -> Log.warn("Server-side error, aborting translation.");
case UNKNOWN_STATUS -> { // most likely rate limit
Log.info("Rate limit reached, retrying...");
servers.put(server, true);
Timer.schedule(() -> servers.put(server, false), 60f);
fetch(api, method, body, success);
}
default -> {
if (servers.size >= 2) {
Log.err("HTTP Response indicates error, retrying...", hse);
servers.remove(server);
fetch(api, method, body, success);
} else {
Log.err("HTTP Response indicates error, disabling translation for this session", hse);
ClientVars.enableTranslation = false;
}
}
}
} else if (e instanceof IOException) {
if (servers.size >= 2) {
Log.err("I/O error, retrying...", e);
servers.remove(server);
fetch(api, method, body, success);
} else {
Log.err("I/O error, disabling translation for this session", e);
ClientVars.enableTranslation = false;
}
} else {
Log.err("An unknown error occurred, disabling translation for this session", e);
ClientVars.enableTranslation = false;
}
Log.info("URL: @\nBody: @", server + api, body);
})
.submit(response -> {
String result = response.getResultAsString();
Log.debug("Response from @:\n@", server, result.replace("\n", ""));
success.get(result);
});
}
}
25 changes: 17 additions & 8 deletions core/src/mindustry/core/NetClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ public static void sendMessage(String message, @Nullable String unformatted, @Nu
// playersender is exactly what you think it is, null for server messages

Color background = null;
String stripped = Strings.stripColors(InvisibleCharCoder.INSTANCE.strip(unformatted != null ? unformatted : message));

if (Core.settings.getBool("logmsgstoconsole") && net.client()) // Make sure we are a client, if we are the server it does this already
Log.log(Log.LogLevel.info, "[Chat] &fi@: @",
"&lc" + (playersender == null ? "Server" : Strings.stripColors(playersender.name)),
"&lw" + stripped
);

if(Vars.ui != null){
var prefix = "";

Expand All @@ -240,12 +248,6 @@ public static void sendMessage(String message, @Nullable String unformatted, @Nu
if (Core.settings.getBool("highlightclientmsg")) background = ClientVars.user;
}

if (Core.settings.getBool("logmsgstoconsole") && net.client()) // Make sure we are a client, if we are the server it does this already
Log.log(Log.LogLevel.info, "[Chat] &fi@: @",
"&lc" + (playersender == null ? "Server" : Strings.stripColors(playersender.name)),
"&lw" + Strings.stripColors(InvisibleCharCoder.INSTANCE.strip(unformatted != null ? unformatted : message))
);

// highlight coords and set as the last position
unformatted = processCoords(unformatted, true);
message = processCoords(message, unformatted != null);
Expand All @@ -267,8 +269,15 @@ public static void sendMessage(String message, @Nullable String unformatted, @Nu

// I don't think this even works
// var unformatted2 = unformatted == null ? StringsKt.removePrefix(message, "[" + playersender.coloredName() + "]: ") : unformatted;
output = ui.chatfrag.addMessage(message, playersender.coloredName(), background, prefix, unformatted);
output.addButton(output.formattedMessage.indexOf(playersender.coloredName()), playersender.coloredName().length() + 16 + output.prefix.length(), () -> Spectate.INSTANCE.spectate(playersender));
var senderName = playersender.coloredName();
output = ui.chatfrag.addMessage(message, senderName, background, prefix, unformatted);
output.addButton(output.formattedMessage.indexOf(senderName), senderName.length() + 16 + output.prefix.length(), () -> Spectate.INSTANCE.spectate(playersender));

if (Core.settings.getBool("enabletranslation") && playersender != player)
Translating.translate(stripped, ClientVars.targetLang, translation -> {
if (!stripped.equals(translation))
ui.chatfrag.addMessage(translation, Core.bundle.get("translation"), ClientVars.translatedColor, "", translation);
});
} else {
// server message, unformatted is ignored
output = ui.chatfrag.addMessage(message, null, null, "", "");
Expand Down
2 changes: 2 additions & 0 deletions core/src/mindustry/ui/dialogs/SettingsMenuDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

import static arc.Core.*;
import static mindustry.Vars.*;
import static mindustry.client.ClientVars.*;

public class SettingsMenuDialog extends BaseDialog{
public SettingsTable graphics, sound, game, main, client, moderation;
Expand Down Expand Up @@ -343,6 +344,7 @@ void addSettings(){
client.category("chat");
client.checkPref("clearchatonleave", true);
client.checkPref("logmsgstoconsole", true);
client.checkPref("enabletranslation", true, b -> enableTranslation = b);
client.checkPref("clientjoinleave", true);
client.checkPref("showidinjoinleave", false);
client.checkPref("highlightcryptomsg", true);
Expand Down