From 9df0e04508332bee8606760f44da662302cf062c Mon Sep 17 00:00:00 2001 From: JFronny Date: Fri, 25 Mar 2022 18:29:34 +0100 Subject: [PATCH] [translate] Implement LibreTranslate support --- .gitlab-ci.yml | 2 +- README.md | 2 +- docs/libjf-config-v0.md | 5 + docs/libjf-translate-v1.md | 41 ++--- .../io/gitlab/jfronny/libjf/HttpUtils.java | 163 ++++++++++++++++++ .../libjf/coprocess/CoProcessManager.java | 6 +- .../client/screen/EntryInfoWidgetBuilder.java | 2 +- libjf-translate-v1/build.gradle | 2 +- .../jfronny/libjf/translate/api/Language.java | 6 + .../libjf/translate/api/TranslateService.java | 32 +++- .../libjf/translate/impl/TranslateConfig.java | 21 +++ ...uage.java => GoogleTranslateLanguage.java} | 24 ++- .../impl/google/GoogleTranslateService.java | 56 +++--- .../libretranslate/LibreTranslateService.java | 114 ++++++++++++ .../model/LibreTranslateDetectResult.java | 6 + .../model/LibreTranslateLanguage.java | 29 ++++ .../model/LibreTranslateRequest.java | 10 ++ .../model/LibreTranslateResult.java | 5 + .../translate/impl/noop/NoopLanguage.java | 17 ++ .../impl/noop/NoopTranslateService.java | 25 ++- .../assets/libjf-translate-v0/lang/en_us.json | 10 ++ .../src/main/resources/fabric.mod.json | 5 +- .../libjf/translate/test/TestEntrypoint.java | 30 +++- 23 files changed, 529 insertions(+), 84 deletions(-) create mode 100644 libjf-base/src/main/java/io/gitlab/jfronny/libjf/HttpUtils.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/Language.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/TranslateConfig.java rename libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/{Language.java => GoogleTranslateLanguage.java} (60%) create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/LibreTranslateService.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateDetectResult.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateLanguage.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateRequest.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateResult.java create mode 100644 libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopLanguage.java create mode 100644 libjf-translate-v1/src/main/resources/assets/libjf-translate-v0/lang/en_us.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 06b6ce2..e6919b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,7 +5,7 @@ pages: image: python:3.8-buster stage: deploy script: - - pip install mkdocs + - pip install mkdocs jinja2==3.0.0 - mkdocs build artifacts: paths: diff --git a/README.md b/README.md index 4ec67ad..b90a2a6 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,6 @@ If one of my mods depends on this, it will be mentioned in its page. Apart from useful classes for mods, LibJF also adds two new item tags: - `raut:overpowered`: if an entity only wears armor items with this tag, it will become invulnerable - `raut:shulker_boxes_illegal`: items with this tag cannot be placed inside shulker boxes. - Inteded to be used for backpacks or similar items + Intended to be used for backpacks or similar items If you want to use LibJF yourself, you can find documentation [here](https://jfmods.gitlab.io/LibJF/) diff --git a/docs/libjf-config-v0.md b/docs/libjf-config-v0.md index 980ba4f..d785a04 100644 --- a/docs/libjf-config-v0.md +++ b/docs/libjf-config-v0.md @@ -40,6 +40,11 @@ ConfigHolder.getInstance().get("yourmod").write(); LibJF config is only intended for simple config screens, it does not support nested classes, multiple pages or controls like sliders. Use something else for those +## Translations +Config keys are translated as `.jfconfig.`. +You may add a tooltip as follows: `.jfconfig..tooltip`. +Enum keys are translated as follows: `.jfconfig.enum..` + ## Presets libjf-config-v0 provides a preset system to automatically fill in certain values based on a function. To add a snippet, add a public static method to your config class and annotate it with @Preset. diff --git a/docs/libjf-translate-v1.md b/docs/libjf-translate-v1.md index a49ed47..e38b31c 100644 --- a/docs/libjf-translate-v1.md +++ b/docs/libjf-translate-v1.md @@ -1,25 +1,26 @@ -# libjf-translate-v0 -libjf-translate-v0 provides a utility class for translating strings through Google Translate. +# libjf-translate-v1 +libjf-translate-v1 provides a utility class for translating strings through user-configurable services. To use this, first obtain a TranslateService instance. You can use `TranslateService.getConfigured()` to do so. -Please be aware that due to the nature of java generics, a workaround as seen in the example may be needed for successful compilation. -You can also directly access implementations, however, this is not recommended. +Please be aware that due to the nature of java generics, using var instead of a specific type for instances is recommended. +You can also directly access implementations, however, this is not recommended and is not subject to the API stability promise. The TranslateService interface exposes all relevant functionality. -Example: -```java -public void onInitialize() { - try { - runTest(TranslateService.getConfigured()); - } catch (Throwable e) { - LibJf.LOGGER.error("Could not verify translation validity", e); - } -} +TranslateService:: -private void runTest(TranslateService ts) throws TranslateException { - final String source = "Cogito, ergo sum"; - final String expected = "I think, therefore I am"; - assert expected.equals(ts.translate(source, ts.detect(source), ts.parseLang("en"))); - assert expected.equals(ts.translate(source, ts.parseLang("la"), ts.parseLang("en"))); -} -``` \ No newline at end of file +| Name | Explanation | +|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| +| `static TranslateService getConfigured()` | Returns the TranslateService the user configured. Implementations may change without notice. | +| `static List> getAvailable()` | Returns all available TranslateServices. Please use getConfigured() instead where possible. | +| `String translate(String textToTranslate, T translateFrom, T translateTo` | Translates a string from the specified source language (or null to auto-detect) to the target language. | +| `T detect(String text)` | Detects the language used in the specified string. | +| `T parseLang(Stirng lang)` | Gets the language for the specified ID | +| `List getAvailableLanguages()` | Get all available languages for the configured service. | +| `String getName()` | Get the name of this translate service. | + +Language: + +| Name | Explanation | +|---------------------------|--------------------------------------------------------------------------| +| `String getDisplayName()` | Returns the string to show in UIs for this language | +| `String getIdentifier()` | Returns the ID for internal use (TranslateService.parseLang for example) | \ No newline at end of file diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/HttpUtils.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/HttpUtils.java new file mode 100644 index 0000000..663f919 --- /dev/null +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/HttpUtils.java @@ -0,0 +1,163 @@ +package io.gitlab.jfronny.libjf; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class HttpUtils { + private static final HttpClient CLIENT = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); + + private enum Method { + GET, POST + } + + public static class Request { + private static final Pattern CURSEFORGE_API = Pattern.compile("(?:http(s)?://)?addons-ecs\\.forgesvc\\.net/api/+"); + private final String url; + private final HttpRequest.Builder builder; + private Method method; + private int sent = 0; + + public Request(Method method, String url) throws URISyntaxException { + this.url = url.replace(" ", "%20"); + this.builder = HttpRequest.newBuilder() + .uri(new URI(this.url)) + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"); + this.method = method; + } + + public Request bearer(String token) { + builder.header("Authorization", "Bearer " + token); + + return this; + } + + public Request header(String name, String value) { + builder.header(name, value); + return this; + } + + public Request bodyString(String string) { + builder.header("Content-Type", "text/plain"); + builder.method(method.name(), HttpRequest.BodyPublishers.ofString(string)); + method = null; + + return this; + } + + public Request bodyForm(String string) { + builder.header("Content-Type", "application/x-www-form-urlencoded"); + builder.method(method.name(), HttpRequest.BodyPublishers.ofString(string)); + method = null; + + return this; + } + + public Request bodyForm(Map entries) { + return bodyForm(entries.entrySet() + .stream() + .map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + '=' + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&"))); + } + + public Request bodyJson(String string) { + builder.header("Content-Type", "application/json"); + builder.method(method.name(), HttpRequest.BodyPublishers.ofString(string)); + method = null; + + return this; + } + + public Request bodyJson(Object object) { + builder.header("Content-Type", "application/json"); + builder.method(method.name(), HttpRequest.BodyPublishers.ofString(LibJf.GSON.toJson(object))); + method = null; + + return this; + } + + private T _send(String accept, HttpResponse.BodyHandler responseBodyHandler) throws IOException { + sent++; + if (sent > 3) throw new IOException("Attempted third redirect, stopping"); + builder.header("Accept", accept); + if (method != null) builder.method(method.name(), HttpRequest.BodyPublishers.noBody()); + + HttpResponse res; + try { + res = CLIENT.send(builder.build(), responseBodyHandler); + } catch (InterruptedException e) { + throw new IOException("Could not send request", e); + } + if (res.statusCode() == 200) return res.body(); + Optional location = res.headers().firstValue("location"); + // Redirect + if (location.isPresent() && (res.statusCode() == 302 || res.statusCode() == 307) && method == Method.GET) { + try { + return HttpUtils.get(location.get())._send(accept, responseBodyHandler); + } catch (URISyntaxException e) { + throw new IOException("Could not follow redirect", e); + } + } + // CurseForge serverside error + if (CURSEFORGE_API.matcher(url).matches() && res.statusCode() >= 500 && res.statusCode() < 600) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new IOException("Could not sleep before resending request" + e); + } + return _send(accept, responseBodyHandler); + } + throw new IOException("Unexpected return method: " + res.statusCode() + " (URL=" + url + ")"); + } + + public void send() throws IOException { + _send("*/*", HttpResponse.BodyHandlers.discarding()); + } + + public InputStream sendInputStream() throws IOException { + return _send("*/*", HttpResponse.BodyHandlers.ofInputStream()); + } + + public String sendString() throws IOException { + return _send("*/*", HttpResponse.BodyHandlers.ofString()); + } + + public Stream sendLines() throws IOException { + return _send("*/*", HttpResponse.BodyHandlers.ofLines()); + } + + public T sendJson(Type type) throws IOException { + InputStream in = _send("application/json", HttpResponse.BodyHandlers.ofInputStream()); + return in == null ? null : LibJf.GSON.fromJson(new InputStreamReader(in), type); + } + + private String getString(Object a) throws IOException { + if (a instanceof InputStream s) return new String(s.readAllBytes()); + if (a instanceof String s) return s; + if (a instanceof Stream s) return ((Stream)s).collect(Collectors.joining()); + return ""; + } + } + + public static Request get(String url) throws URISyntaxException { + return new Request(Method.GET, url); + } + + public static Request post(String url) throws URISyntaxException { + return new Request(Method.POST, url); + } +} diff --git a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java index e67fd7f..c6b7f00 100644 --- a/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java +++ b/libjf-base/src/main/java/io/gitlab/jfronny/libjf/coprocess/CoProcessManager.java @@ -9,6 +9,7 @@ import net.fabricmc.loader.api.FabricLoader; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; public class CoProcessManager implements ModInitializer { @@ -28,7 +29,9 @@ public class CoProcessManager implements ModInitializer { } private void stop() { - for (CoProcess coProcess : coProcesses) { + Iterator procs = coProcesses.iterator(); + while (procs.hasNext()) { + CoProcess coProcess = procs.next(); coProcess.stop(); if (coProcess instanceof Closeable cl) { try { @@ -37,6 +40,7 @@ public class CoProcessManager implements ModInitializer { LibJf.LOGGER.error("Could not close co-process", e); } } + procs.remove(); } } } diff --git a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java index 4a9a4f3..7a4366d 100644 --- a/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java +++ b/libjf-config-v0/src/main/java/io/gitlab/jfronny/libjf/config/impl/client/screen/EntryInfoWidgetBuilder.java @@ -55,7 +55,7 @@ public class EntryInfoWidgetBuilder { }, func); } else if (type.isEnum()) { List values = Arrays.asList(info.field.getType().getEnumConstants()); - Function func = value -> new TranslatableText(config.getModId() + ".jfconfig." + "enum." + type.getSimpleName() + "." + info.value.toString()); + Function func = value -> new TranslatableText(config.getModId() + ".jfconfig.enum." + type.getSimpleName() + "." + info.value.toString()); info.widget = new AbstractMap.SimpleEntry>(button -> { int index = values.indexOf(info.value) + 1; info.value = values.get(index >= values.size()? 0 : index); diff --git a/libjf-translate-v1/build.gradle b/libjf-translate-v1/build.gradle index a71f7d5..4ac20f6 100644 --- a/libjf-translate-v1/build.gradle +++ b/libjf-translate-v1/build.gradle @@ -1,5 +1,5 @@ archivesBaseName = "libjf-translate-v1" dependencies { - moduleDependencies(project, ["libjf-base"]) + moduleDependencies(project, ["libjf-base", "libjf-config-v0"]) } \ No newline at end of file diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/Language.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/Language.java new file mode 100644 index 0000000..b42d087 --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/Language.java @@ -0,0 +1,6 @@ +package io.gitlab.jfronny.libjf.translate.api; + +public interface Language { + String getDisplayName(); + String getIdentifier(); +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/TranslateService.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/TranslateService.java index b562a71..2285002 100644 --- a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/TranslateService.java +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/api/TranslateService.java @@ -1,15 +1,43 @@ package io.gitlab.jfronny.libjf.translate.api; +import io.gitlab.jfronny.libjf.LibJf; +import io.gitlab.jfronny.libjf.translate.impl.TranslateConfig; import io.gitlab.jfronny.libjf.translate.impl.google.GoogleTranslateService; +import io.gitlab.jfronny.libjf.translate.impl.libretranslate.LibreTranslateService; +import io.gitlab.jfronny.libjf.translate.impl.noop.NoopTranslateService; import java.util.List; -public interface TranslateService { +public interface TranslateService { static TranslateService getConfigured() { - return new GoogleTranslateService(); + return switch (TranslateConfig.translationService) { + case Noop -> NoopTranslateService.INSTANCE; + case Google -> GoogleTranslateService.INSTANCE; + case LibreTranslate -> { + try { + yield LibreTranslateService.get(TranslateConfig.libreTranslateHost); + } catch (TranslateException e) { + LibJf.LOGGER.error("Could not use the specified LibreTranslate host, using NOOP", e); + yield NoopTranslateService.INSTANCE; + } + } + }; } + + static List> getAvailable() { + LibreTranslateService lts = null; + try { + lts = LibreTranslateService.get(TranslateConfig.libreTranslateHost); + } catch (TranslateException ignored) { + } + return lts == null + ? List.of(GoogleTranslateService.INSTANCE, NoopTranslateService.INSTANCE) + : List.of(GoogleTranslateService.INSTANCE, lts, NoopTranslateService.INSTANCE); + } + String translate(String textToTranslate, T translateFrom, T translateTo) throws TranslateException; T detect(String text) throws TranslateException; T parseLang(String lang); List getAvailableLanguages(); + String getName(); } diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/TranslateConfig.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/TranslateConfig.java new file mode 100644 index 0000000..a813ea0 --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/TranslateConfig.java @@ -0,0 +1,21 @@ +package io.gitlab.jfronny.libjf.translate.impl; + +import io.gitlab.jfronny.libjf.config.api.Entry; +import io.gitlab.jfronny.libjf.config.api.JfConfig; +import io.gitlab.jfronny.libjf.config.api.Verifier; + +public class TranslateConfig implements JfConfig { + @Entry public static Translator translationService = Translator.Google; + @Entry public static String libreTranslateHost = "https://translate.argosopentech.com"; + + @Verifier + public static void ensureValid() { + if (translationService == null) translationService = Translator.Google; + if (translationService == Translator.LibreTranslate && libreTranslateHost == null || libreTranslateHost.isBlank()) + libreTranslateHost = "https://translate.argosopentech.com"; + } + + public enum Translator { + Google, LibreTranslate, Noop + } +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/Language.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateLanguage.java similarity index 60% rename from libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/Language.java rename to libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateLanguage.java index f3280bd..687cc64 100644 --- a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/Language.java +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateLanguage.java @@ -3,7 +3,7 @@ package io.gitlab.jfronny.libjf.translate.impl.google; import java.util.HashMap; import java.util.Map; -public enum Language { +public enum GoogleTranslateLanguage implements io.gitlab.jfronny.libjf.translate.api.Language { AUTO_DETECT("AUTO_DETECT", "auto"), ARABIC("ARABIC", "ar"), CHINESE_SIMPLIFIED("CHINESE_SIMPLIFIED", "zh-CN"), CHINESE_TRADITIONAL("CHINESE_TRADITIONAL", "zh-TW"), ENGLISH("ENGLISH", "en"), FILIPINO("FILIPINO", "tl"), FRENCH("FRENCH", "fr"), GERMAN("GERMAN", "de"), GREEK("GREEK", "el"), INDONESIAN("INDONESIAN", "id"), @@ -12,22 +12,22 @@ public enum Language { RUSSIAN("RUSSIAN", "ru"), SPANISH("SPANISH", "es"), SWEDISH("SWEDISH", "sv"), THAI("THAI", "th"), VIETNAMESE("VIETNAMESE", "vi"); - private static final Map LANGUAGE_BY_VALUE = new HashMap<>(); + private static final Map LANGUAGE_BY_ID = new HashMap<>(); static { - for (Language language : Language.values()) { - LANGUAGE_BY_VALUE.put(language.id, language); + for (GoogleTranslateLanguage language : GoogleTranslateLanguage.values()) { + LANGUAGE_BY_ID.put(language.id, language); } } - public static Language byId(String value) { - return LANGUAGE_BY_VALUE.getOrDefault(value, AUTO_DETECT); + public static GoogleTranslateLanguage byId(String value) { + return LANGUAGE_BY_ID.getOrDefault(value, AUTO_DETECT); } public final String name; public final String id; - Language(String name, String id) { + GoogleTranslateLanguage(String name, String id) { this.name = name; this.id = id; } @@ -36,4 +36,14 @@ public enum Language { public String toString() { return name; } + + @Override + public String getDisplayName() { + return name; + } + + @Override + public String getIdentifier() { + return id; + } } diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateService.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateService.java index fe3d31d..2f8b2ed 100644 --- a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateService.java +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/google/GoogleTranslateService.java @@ -1,14 +1,11 @@ package io.gitlab.jfronny.libjf.translate.impl.google; +import io.gitlab.jfronny.libjf.HttpUtils; import io.gitlab.jfronny.libjf.translate.api.TranslateException; import io.gitlab.jfronny.libjf.translate.api.TranslateService; import org.apache.commons.lang3.StringEscapeUtils; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -19,12 +16,18 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class GoogleTranslateService implements TranslateService { +public class GoogleTranslateService implements TranslateService { + public static final GoogleTranslateService INSTANCE = new GoogleTranslateService(); private static final Pattern TRANSLATION_RESULT = Pattern.compile("class=\"result-container\">([^<]*)", Pattern.MULTILINE); + private GoogleTranslateService() { + } + @Override - public String translate(String textToTranslate, Language translateFrom, Language translateTo) throws TranslateException { + public String translate(String textToTranslate, GoogleTranslateLanguage translateFrom, GoogleTranslateLanguage translateTo) throws TranslateException { if (textToTranslate == null) throw new TranslateException("textToTranslate must not be null"); + if (translateFrom == null) translateFrom = GoogleTranslateLanguage.AUTO_DETECT; + if (translateTo == null) throw new TranslateException("translateTo must not be null"); String pageSource = ""; try { pageSource = getPageSource(textToTranslate, translateFrom.id, translateTo.id); @@ -48,45 +51,32 @@ public class GoogleTranslateService implements TranslateService { } @Override - public Language detect(String text) throws TranslateException { - return Language.AUTO_DETECT; + public GoogleTranslateLanguage detect(String text) throws TranslateException { + return GoogleTranslateLanguage.AUTO_DETECT; } @Override - public Language parseLang(String lang) { - return Language.byId(lang); + public GoogleTranslateLanguage parseLang(String lang) { + return GoogleTranslateLanguage.byId(lang); } @Override - public List getAvailableLanguages() { - List langs = new ArrayList<>(Arrays.asList(Language.values())); - langs.remove(Language.AUTO_DETECT); + public List getAvailableLanguages() { + List langs = new ArrayList<>(Arrays.asList(GoogleTranslateLanguage.values())); + langs.remove(GoogleTranslateLanguage.AUTO_DETECT); return langs; } - private static String getPageSource(String textToTranslate, String translateFrom, String translateTo) - throws Exception { + @Override + public String getName() { + return "Google"; + } + + private static String getPageSource(String textToTranslate, String translateFrom, String translateTo) throws Exception { if (textToTranslate == null) return null; String pageUrl = String.format("https://translate.google.com/m?hl=en&sl=%s&tl=%s&ie=UTF-8&prev=_m&q=%s", translateFrom, translateTo, URLEncoder.encode(textToTranslate.trim(), StandardCharsets.UTF_8)); - URL url = new URL(pageUrl); - HttpURLConnection connection = null; - StringBuilder pageSource = new StringBuilder(); - try { - connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(5000); - connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.95 Safari/537.11"); - try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = bufferedReader.readLine()) != null) { - pageSource.append(line).append('\n'); - } - } - return pageSource.toString(); - } finally { - if (connection != null) - connection.disconnect(); - } + return HttpUtils.get(pageUrl).sendString(); } } diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/LibreTranslateService.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/LibreTranslateService.java new file mode 100644 index 0000000..76053e8 --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/LibreTranslateService.java @@ -0,0 +1,114 @@ +package io.gitlab.jfronny.libjf.translate.impl.libretranslate; + +import com.google.common.reflect.TypeToken; +import io.gitlab.jfronny.libjf.HttpUtils; +import io.gitlab.jfronny.libjf.translate.api.TranslateException; +import io.gitlab.jfronny.libjf.translate.api.TranslateService; +import io.gitlab.jfronny.libjf.translate.impl.libretranslate.model.LibreTranslateDetectResult; +import io.gitlab.jfronny.libjf.translate.impl.libretranslate.model.LibreTranslateLanguage; +import io.gitlab.jfronny.libjf.translate.impl.libretranslate.model.LibreTranslateRequest; +import io.gitlab.jfronny.libjf.translate.impl.libretranslate.model.LibreTranslateResult; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class LibreTranslateService implements TranslateService { + private static final Type languageListType = new TypeToken>(){}.getType(); + private static final Type translateDetectResultListType = new TypeToken>(){}.getType(); + private static final LibreTranslateLanguage autoDetect = new LibreTranslateLanguage("auto", "AUTO_DETECT"); + private static final Map knownInstances = new HashMap<>(); + + public static LibreTranslateService get(String host) throws TranslateException { + LibreTranslateService lts; + if (knownInstances.containsKey(host)) { + lts = knownInstances.get(host); + if (lts == null) throw new TranslateException("Translate service previously failed to initialize. Not trying again"); + return lts; + } + try { + lts = new LibreTranslateService(host); + } catch (TranslateException e) { + knownInstances.put(host, null); + throw new TranslateException("Could not instantiate translate service", e); + } + knownInstances.put(host, lts); + return lts; + } + + private final String host; + private final List knownLanguages; + private final Map languageById = new HashMap<>(); + private LibreTranslateService(String host) throws TranslateException { + if (host.endsWith("/")) host = host.substring(0, host.length() - 1); + this.host = host; + try { + ArrayList langs = new ArrayList<>(); + langs.add(autoDetect); + for (LibreTranslateLanguage.ApiResult lang : HttpUtils.get(host + "/languages").>sendJson(languageListType)) { + LibreTranslateLanguage langR = lang.toLanguage(); + langs.add(langR); + languageById.put(lang.code, langR); + } + this.knownLanguages = List.copyOf(langs); + } catch (IOException | URISyntaxException e) { + throw new TranslateException("Could not get known languages for LibreTranslate backend", e); + } + } + + @Override + public String translate(String textToTranslate, LibreTranslateLanguage translateFrom, LibreTranslateLanguage translateTo) throws TranslateException { + if (textToTranslate == null) throw new TranslateException("textToTranslate must not be null"); + if (translateFrom == null) translateFrom = autoDetect; + if (translateTo == null) throw new TranslateException("translateTo must not be null"); + LibreTranslateRequest.Translate request = new LibreTranslateRequest.Translate(); + request.q = textToTranslate; + request.source = translateFrom.getIdentifier(); + request.target = translateTo.getIdentifier(); + LibreTranslateResult result = null; + try { + result = HttpUtils.post(host + "/translate").bodyJson(request).sendJson(LibreTranslateResult.class); + } catch (IOException | URISyntaxException e) { + throw new TranslateException("Could not translate text", e); + } + return result.translatedText; + } + + @Override + public LibreTranslateLanguage detect(String text) throws TranslateException { + LibreTranslateRequest request = new LibreTranslateRequest(); + request.q = text; + List result; + try { + result = HttpUtils.post(host + "/detect").bodyJson(request).sendJson(translateDetectResultListType); + } catch (IOException | URISyntaxException e) { + throw new TranslateException("Could not detect language", e); + } + LibreTranslateDetectResult resCurr = null; + for (LibreTranslateDetectResult res : result) { + if (resCurr == null || res.confidence > resCurr.confidence) + resCurr = res; + } + if (resCurr == null) throw new TranslateException("Could not identify any valid language"); + return parseLang(resCurr.language); + } + + @Override + public LibreTranslateLanguage parseLang(String lang) { + return languageById.get(lang); + } + + @Override + public List getAvailableLanguages() { + return knownLanguages; + } + + @Override + public String getName() { + return "LibreTranslate"; + } +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateDetectResult.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateDetectResult.java new file mode 100644 index 0000000..87a067e --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateDetectResult.java @@ -0,0 +1,6 @@ +package io.gitlab.jfronny.libjf.translate.impl.libretranslate.model; + +public class LibreTranslateDetectResult { + public float confidence; + public String language; +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateLanguage.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateLanguage.java new file mode 100644 index 0000000..af32977 --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateLanguage.java @@ -0,0 +1,29 @@ +package io.gitlab.jfronny.libjf.translate.impl.libretranslate.model; + +import io.gitlab.jfronny.libjf.translate.api.Language; + +public record LibreTranslateLanguage(String code, String name) implements Language { + @Override + public String toString() { + return name; + } + + @Override + public String getDisplayName() { + return name; + } + + @Override + public String getIdentifier() { + return code; + } + + public static class ApiResult { + public String code; + public String name; + + public LibreTranslateLanguage toLanguage() { + return new LibreTranslateLanguage(code, name); + } + } +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateRequest.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateRequest.java new file mode 100644 index 0000000..fc4f29a --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateRequest.java @@ -0,0 +1,10 @@ +package io.gitlab.jfronny.libjf.translate.impl.libretranslate.model; + +public class LibreTranslateRequest { + public String q; + + public static class Translate extends LibreTranslateRequest { + public String source; + public String target; + } +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateResult.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateResult.java new file mode 100644 index 0000000..c09a018 --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/libretranslate/model/LibreTranslateResult.java @@ -0,0 +1,5 @@ +package io.gitlab.jfronny.libjf.translate.impl.libretranslate.model; + +public class LibreTranslateResult { + public String translatedText; +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopLanguage.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopLanguage.java new file mode 100644 index 0000000..883467f --- /dev/null +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopLanguage.java @@ -0,0 +1,17 @@ +package io.gitlab.jfronny.libjf.translate.impl.noop; + +import io.gitlab.jfronny.libjf.translate.api.Language; + +public class NoopLanguage implements Language { + static final NoopLanguage INSTANCE = new NoopLanguage(); + + @Override + public String getDisplayName() { + return "none"; + } + + @Override + public String getIdentifier() { + return "none"; + } +} diff --git a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopTranslateService.java b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopTranslateService.java index da9e127..14713af 100644 --- a/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopTranslateService.java +++ b/libjf-translate-v1/src/main/java/io/gitlab/jfronny/libjf/translate/impl/noop/NoopTranslateService.java @@ -5,24 +5,33 @@ import io.gitlab.jfronny.libjf.translate.api.TranslateService; import java.util.List; -public class NoopTranslateService implements TranslateService { +public class NoopTranslateService implements TranslateService { + public static final NoopTranslateService INSTANCE = new NoopTranslateService(); + private NoopTranslateService() { + } + @Override - public String translate(String textToTranslate, Object translateFrom, Object translateTo) throws TranslateException { + public String translate(String textToTranslate, NoopLanguage translateFrom, NoopLanguage translateTo) throws TranslateException { return textToTranslate; } @Override - public Object detect(String text) throws TranslateException { - return "none"; + public NoopLanguage detect(String text) throws TranslateException { + return NoopLanguage.INSTANCE; } @Override - public Object parseLang(String lang) { - return "none"; + public NoopLanguage parseLang(String lang) { + return NoopLanguage.INSTANCE; } @Override - public List getAvailableLanguages() { - return List.of("none"); + public List getAvailableLanguages() { + return List.of(NoopLanguage.INSTANCE); + } + + @Override + public String getName() { + return "Noop"; } } diff --git a/libjf-translate-v1/src/main/resources/assets/libjf-translate-v0/lang/en_us.json b/libjf-translate-v1/src/main/resources/assets/libjf-translate-v0/lang/en_us.json new file mode 100644 index 0000000..b413f8b --- /dev/null +++ b/libjf-translate-v1/src/main/resources/assets/libjf-translate-v0/lang/en_us.json @@ -0,0 +1,10 @@ +{ + "libjf-translate-v1.jfconfig.title": "LibJF Translate v1", + "libjf-translate-v1.jfconfig.translationService": "Translation Service", + "libjf-translate-v1.jfconfig.translationService.tooltip": "The service to use for translation. Other mods may access services directly, but this should be used", + "libjf-translate-v1.jfconfig.libreTranslateHost": "LibreTranslate Host", + "libjf-translate-v1.jfconfig.libreTranslateHost.tooltip": "The host of LibreTranslate to use if that is selected", + "libjf-translate-v1.jfconfig.enum.Translator.Google": "Google", + "libjf-translate-v1.jfconfig.enum.Translator.LibreTranslate": "LibreTranslate", + "libjf-translate-v1.jfconfig.enum.Translator.Noop": "NOOP" +} diff --git a/libjf-translate-v1/src/main/resources/fabric.mod.json b/libjf-translate-v1/src/main/resources/fabric.mod.json index 60ca630..68c12c6 100644 --- a/libjf-translate-v1/src/main/resources/fabric.mod.json +++ b/libjf-translate-v1/src/main/resources/fabric.mod.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "id": "libjf-translate-v0", + "id": "libjf-translate-v1", "name": "LibJF Translate", "version": "${version}", "environment": "*", @@ -17,6 +17,9 @@ "minecraft": "*", "libjf-base": ">=${version}" }, + "entrypoints": { + "libjf:config": ["io.gitlab.jfronny.libjf.translate.impl.TranslateConfig"] + }, "custom": { "modmenu": { "parent": "libjf", diff --git a/libjf-translate-v1/src/testmod/java/io/gitlab/jfronny/libjf/translate/test/TestEntrypoint.java b/libjf-translate-v1/src/testmod/java/io/gitlab/jfronny/libjf/translate/test/TestEntrypoint.java index 864de17..43e1ebd 100644 --- a/libjf-translate-v1/src/testmod/java/io/gitlab/jfronny/libjf/translate/test/TestEntrypoint.java +++ b/libjf-translate-v1/src/testmod/java/io/gitlab/jfronny/libjf/translate/test/TestEntrypoint.java @@ -1,24 +1,38 @@ package io.gitlab.jfronny.libjf.translate.test; import io.gitlab.jfronny.libjf.LibJf; -import io.gitlab.jfronny.libjf.translate.api.TranslateService; -import io.gitlab.jfronny.libjf.translate.api.TranslateException; +import io.gitlab.jfronny.libjf.translate.impl.google.GoogleTranslateService; +import io.gitlab.jfronny.libjf.translate.impl.libretranslate.LibreTranslateService; import net.fabricmc.api.ModInitializer; +import java.util.Objects; + public class TestEntrypoint implements ModInitializer { @Override public void onInitialize() { try { - runTest(TranslateService.getConfigured()); + { + GoogleTranslateService ts = GoogleTranslateService.INSTANCE; + LibJf.LOGGER.info("Testing Google Translate"); + final String sourceLA = "Cogito, ergo sum"; + assertEqual("auto", ts.detect(sourceLA).getIdentifier()); + assertEqual("I think, therefore I am", ts.translate(sourceLA, ts.parseLang("la"), ts.parseLang("en"))); + } + + { + LibreTranslateService ts = LibreTranslateService.get("https://translate.argosopentech.com"); + LibJf.LOGGER.info("Testing LibreTranslate"); + final String sourceEN = "Hello, World!"; + assertEqual("en", ts.detect(sourceEN).getIdentifier()); + assertEqual("Hallo, Welt!", ts.translate(sourceEN, ts.parseLang("en"), ts.parseLang("de"))); + } } catch (Throwable e) { LibJf.LOGGER.error("Could not verify translation validity", e); } } - private void runTest(TranslateService ts) throws TranslateException { - final String source = "Cogito, ergo sum"; - final String expected = "I think, therefore I am"; - assert expected.equals(ts.translate(source, ts.detect(source), ts.parseLang("en"))); - assert expected.equals(ts.translate(source, ts.parseLang("la"), ts.parseLang("en"))); + private static void assertEqual(Object o1, Object o2) { + if (!Objects.equals(o1, o2)) + throw new AssertionError("Assertion not met: expected " + o1 + " but got " + o2); } }