[translate] Implement LibreTranslate support
This commit is contained in:
parent
dcc3dec8f1
commit
9df0e04508
|
@ -5,7 +5,7 @@ pages:
|
||||||
image: python:3.8-buster
|
image: python:3.8-buster
|
||||||
stage: deploy
|
stage: deploy
|
||||||
script:
|
script:
|
||||||
- pip install mkdocs
|
- pip install mkdocs jinja2==3.0.0
|
||||||
- mkdocs build
|
- mkdocs build
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
|
|
|
@ -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:
|
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: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.
|
- `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/)
|
If you want to use LibJF yourself, you can find documentation [here](https://jfmods.gitlab.io/LibJF/)
|
||||||
|
|
|
@ -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.
|
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
|
Use something else for those
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
Config keys are translated as `<mod id>.jfconfig.<field name>`.
|
||||||
|
You may add a tooltip as follows: `<mod id>.jfconfig.<field name>.tooltip`.
|
||||||
|
Enum keys are translated as follows: `<mod id>.jfconfig.enum.<enum class name>.<entry name>`
|
||||||
|
|
||||||
## Presets
|
## Presets
|
||||||
libjf-config-v0 provides a preset system to automatically fill in certain values based on a function.
|
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.
|
To add a snippet, add a public static method to your config class and annotate it with @Preset.
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
# libjf-translate-v0
|
# libjf-translate-v1
|
||||||
libjf-translate-v0 provides a utility class for translating strings through Google Translate.
|
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.
|
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.
|
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.
|
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.
|
The TranslateService interface exposes all relevant functionality.
|
||||||
|
|
||||||
Example:
|
TranslateService::
|
||||||
```java
|
|
||||||
public void onInitialize() {
|
|
||||||
try {
|
|
||||||
runTest(TranslateService.getConfigured());
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LibJf.LOGGER.error("Could not verify translation validity", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T> void runTest(TranslateService<T> ts) throws TranslateException {
|
| Name | Explanation |
|
||||||
final String source = "Cogito, ergo sum";
|
|---------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------|
|
||||||
final String expected = "I think, therefore I am";
|
| `static TranslateService<?> getConfigured()` | Returns the TranslateService the user configured. Implementations may change without notice. |
|
||||||
assert expected.equals(ts.translate(source, ts.detect(source), ts.parseLang("en")));
|
| `static List<TranslateService<?>> getAvailable()` | Returns all available TranslateServices. Please use getConfigured() instead where possible. |
|
||||||
assert expected.equals(ts.translate(source, ts.parseLang("la"), ts.parseLang("en")));
|
| `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<T> 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) |
|
|
@ -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<String, String> 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> T _send(String accept, HttpResponse.BodyHandler<T> 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<T> 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<String> 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<String> sendLines() throws IOException {
|
||||||
|
return _send("*/*", HttpResponse.BodyHandlers.ofLines());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> 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<String>)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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import net.fabricmc.loader.api.FabricLoader;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class CoProcessManager implements ModInitializer {
|
public class CoProcessManager implements ModInitializer {
|
||||||
|
@ -28,7 +29,9 @@ public class CoProcessManager implements ModInitializer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void stop() {
|
private void stop() {
|
||||||
for (CoProcess coProcess : coProcesses) {
|
Iterator<CoProcess> procs = coProcesses.iterator();
|
||||||
|
while (procs.hasNext()) {
|
||||||
|
CoProcess coProcess = procs.next();
|
||||||
coProcess.stop();
|
coProcess.stop();
|
||||||
if (coProcess instanceof Closeable cl) {
|
if (coProcess instanceof Closeable cl) {
|
||||||
try {
|
try {
|
||||||
|
@ -37,6 +40,7 @@ public class CoProcessManager implements ModInitializer {
|
||||||
LibJf.LOGGER.error("Could not close co-process", e);
|
LibJf.LOGGER.error("Could not close co-process", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
procs.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ public class EntryInfoWidgetBuilder {
|
||||||
}, func);
|
}, func);
|
||||||
} else if (type.isEnum()) {
|
} else if (type.isEnum()) {
|
||||||
List<?> values = Arrays.asList(info.field.getType().getEnumConstants());
|
List<?> values = Arrays.asList(info.field.getType().getEnumConstants());
|
||||||
Function<Object,Text> func = value -> new TranslatableText(config.getModId() + ".jfconfig." + "enum." + type.getSimpleName() + "." + info.value.toString());
|
Function<Object,Text> func = value -> new TranslatableText(config.getModId() + ".jfconfig.enum." + type.getSimpleName() + "." + info.value.toString());
|
||||||
info.widget = new AbstractMap.SimpleEntry<ButtonWidget.PressAction, Function<Object, Text>>(button -> {
|
info.widget = new AbstractMap.SimpleEntry<ButtonWidget.PressAction, Function<Object, Text>>(button -> {
|
||||||
int index = values.indexOf(info.value) + 1;
|
int index = values.indexOf(info.value) + 1;
|
||||||
info.value = values.get(index >= values.size()? 0 : index);
|
info.value = values.get(index >= values.size()? 0 : index);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
archivesBaseName = "libjf-translate-v1"
|
archivesBaseName = "libjf-translate-v1"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
moduleDependencies(project, ["libjf-base"])
|
moduleDependencies(project, ["libjf-base", "libjf-config-v0"])
|
||||||
}
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.gitlab.jfronny.libjf.translate.api;
|
||||||
|
|
||||||
|
public interface Language {
|
||||||
|
String getDisplayName();
|
||||||
|
String getIdentifier();
|
||||||
|
}
|
|
@ -1,15 +1,43 @@
|
||||||
package io.gitlab.jfronny.libjf.translate.api;
|
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.google.GoogleTranslateService;
|
||||||
|
import io.gitlab.jfronny.libjf.translate.impl.libretranslate.LibreTranslateService;
|
||||||
|
import io.gitlab.jfronny.libjf.translate.impl.noop.NoopTranslateService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface TranslateService<T> {
|
public interface TranslateService<T extends Language> {
|
||||||
static TranslateService<?> getConfigured() {
|
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<TranslateService<?>> 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;
|
String translate(String textToTranslate, T translateFrom, T translateTo) throws TranslateException;
|
||||||
T detect(String text) throws TranslateException;
|
T detect(String text) throws TranslateException;
|
||||||
T parseLang(String lang);
|
T parseLang(String lang);
|
||||||
List<T> getAvailableLanguages();
|
List<T> getAvailableLanguages();
|
||||||
|
String getName();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package io.gitlab.jfronny.libjf.translate.impl.google;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
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"),
|
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"),
|
CHINESE_TRADITIONAL("CHINESE_TRADITIONAL", "zh-TW"), ENGLISH("ENGLISH", "en"), FILIPINO("FILIPINO", "tl"),
|
||||||
FRENCH("FRENCH", "fr"), GERMAN("GERMAN", "de"), GREEK("GREEK", "el"), INDONESIAN("INDONESIAN", "id"),
|
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"),
|
RUSSIAN("RUSSIAN", "ru"), SPANISH("SPANISH", "es"), SWEDISH("SWEDISH", "sv"), THAI("THAI", "th"),
|
||||||
VIETNAMESE("VIETNAMESE", "vi");
|
VIETNAMESE("VIETNAMESE", "vi");
|
||||||
|
|
||||||
private static final Map<String, Language> LANGUAGE_BY_VALUE = new HashMap<>();
|
private static final Map<String, GoogleTranslateLanguage> LANGUAGE_BY_ID = new HashMap<>();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
for (Language language : Language.values()) {
|
for (GoogleTranslateLanguage language : GoogleTranslateLanguage.values()) {
|
||||||
LANGUAGE_BY_VALUE.put(language.id, language);
|
LANGUAGE_BY_ID.put(language.id, language);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Language byId(String value) {
|
public static GoogleTranslateLanguage byId(String value) {
|
||||||
return LANGUAGE_BY_VALUE.getOrDefault(value, AUTO_DETECT);
|
return LANGUAGE_BY_ID.getOrDefault(value, AUTO_DETECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final String name;
|
public final String name;
|
||||||
public final String id;
|
public final String id;
|
||||||
|
|
||||||
Language(String name, String id) {
|
GoogleTranslateLanguage(String name, String id) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
@ -36,4 +36,14 @@ public enum Language {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,14 +1,11 @@
|
||||||
package io.gitlab.jfronny.libjf.translate.impl.google;
|
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.TranslateException;
|
||||||
import io.gitlab.jfronny.libjf.translate.api.TranslateService;
|
import io.gitlab.jfronny.libjf.translate.api.TranslateService;
|
||||||
import org.apache.commons.lang3.StringEscapeUtils;
|
import org.apache.commons.lang3.StringEscapeUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
@ -19,12 +16,18 @@ import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class GoogleTranslateService implements TranslateService<Language> {
|
public class GoogleTranslateService implements TranslateService<GoogleTranslateLanguage> {
|
||||||
|
public static final GoogleTranslateService INSTANCE = new GoogleTranslateService();
|
||||||
private static final Pattern TRANSLATION_RESULT = Pattern.compile("class=\"result-container\">([^<]*)</div>", Pattern.MULTILINE);
|
private static final Pattern TRANSLATION_RESULT = Pattern.compile("class=\"result-container\">([^<]*)</div>", Pattern.MULTILINE);
|
||||||
|
|
||||||
|
private GoogleTranslateService() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@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 (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 = "";
|
String pageSource = "";
|
||||||
try {
|
try {
|
||||||
pageSource = getPageSource(textToTranslate, translateFrom.id, translateTo.id);
|
pageSource = getPageSource(textToTranslate, translateFrom.id, translateTo.id);
|
||||||
|
@ -48,45 +51,32 @@ public class GoogleTranslateService implements TranslateService<Language> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Language detect(String text) throws TranslateException {
|
public GoogleTranslateLanguage detect(String text) throws TranslateException {
|
||||||
return Language.AUTO_DETECT;
|
return GoogleTranslateLanguage.AUTO_DETECT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Language parseLang(String lang) {
|
public GoogleTranslateLanguage parseLang(String lang) {
|
||||||
return Language.byId(lang);
|
return GoogleTranslateLanguage.byId(lang);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Language> getAvailableLanguages() {
|
public List<GoogleTranslateLanguage> getAvailableLanguages() {
|
||||||
List<Language> langs = new ArrayList<>(Arrays.asList(Language.values()));
|
List<GoogleTranslateLanguage> langs = new ArrayList<>(Arrays.asList(GoogleTranslateLanguage.values()));
|
||||||
langs.remove(Language.AUTO_DETECT);
|
langs.remove(GoogleTranslateLanguage.AUTO_DETECT);
|
||||||
return langs;
|
return langs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getPageSource(String textToTranslate, String translateFrom, String translateTo)
|
@Override
|
||||||
throws Exception {
|
public String getName() {
|
||||||
|
return "Google";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getPageSource(String textToTranslate, String translateFrom, String translateTo) throws Exception {
|
||||||
if (textToTranslate == null)
|
if (textToTranslate == null)
|
||||||
return 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",
|
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));
|
translateFrom, translateTo, URLEncoder.encode(textToTranslate.trim(), StandardCharsets.UTF_8));
|
||||||
URL url = new URL(pageUrl);
|
return HttpUtils.get(pageUrl).sendString();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<LibreTranslateLanguage> {
|
||||||
|
private static final Type languageListType = new TypeToken<List<LibreTranslateLanguage.ApiResult>>(){}.getType();
|
||||||
|
private static final Type translateDetectResultListType = new TypeToken<List<LibreTranslateDetectResult>>(){}.getType();
|
||||||
|
private static final LibreTranslateLanguage autoDetect = new LibreTranslateLanguage("auto", "AUTO_DETECT");
|
||||||
|
private static final Map<String, LibreTranslateService> 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<LibreTranslateLanguage> knownLanguages;
|
||||||
|
private final Map<String, LibreTranslateLanguage> languageById = new HashMap<>();
|
||||||
|
private LibreTranslateService(String host) throws TranslateException {
|
||||||
|
if (host.endsWith("/")) host = host.substring(0, host.length() - 1);
|
||||||
|
this.host = host;
|
||||||
|
try {
|
||||||
|
ArrayList<LibreTranslateLanguage> langs = new ArrayList<>();
|
||||||
|
langs.add(autoDetect);
|
||||||
|
for (LibreTranslateLanguage.ApiResult lang : HttpUtils.get(host + "/languages").<ArrayList<LibreTranslateLanguage.ApiResult>>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<LibreTranslateDetectResult> 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<LibreTranslateLanguage> getAvailableLanguages() {
|
||||||
|
return knownLanguages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "LibreTranslate";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package io.gitlab.jfronny.libjf.translate.impl.libretranslate.model;
|
||||||
|
|
||||||
|
public class LibreTranslateDetectResult {
|
||||||
|
public float confidence;
|
||||||
|
public String language;
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.gitlab.jfronny.libjf.translate.impl.libretranslate.model;
|
||||||
|
|
||||||
|
public class LibreTranslateResult {
|
||||||
|
public String translatedText;
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,24 +5,33 @@ import io.gitlab.jfronny.libjf.translate.api.TranslateService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class NoopTranslateService implements TranslateService<Object> {
|
public class NoopTranslateService implements TranslateService<NoopLanguage> {
|
||||||
|
public static final NoopTranslateService INSTANCE = new NoopTranslateService();
|
||||||
|
private NoopTranslateService() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@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;
|
return textToTranslate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object detect(String text) throws TranslateException {
|
public NoopLanguage detect(String text) throws TranslateException {
|
||||||
return "none";
|
return NoopLanguage.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object parseLang(String lang) {
|
public NoopLanguage parseLang(String lang) {
|
||||||
return "none";
|
return NoopLanguage.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Object> getAvailableLanguages() {
|
public List<NoopLanguage> getAvailableLanguages() {
|
||||||
return List.of("none");
|
return List.of(NoopLanguage.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Noop";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
"id": "libjf-translate-v0",
|
"id": "libjf-translate-v1",
|
||||||
"name": "LibJF Translate",
|
"name": "LibJF Translate",
|
||||||
"version": "${version}",
|
"version": "${version}",
|
||||||
"environment": "*",
|
"environment": "*",
|
||||||
|
@ -17,6 +17,9 @@
|
||||||
"minecraft": "*",
|
"minecraft": "*",
|
||||||
"libjf-base": ">=${version}"
|
"libjf-base": ">=${version}"
|
||||||
},
|
},
|
||||||
|
"entrypoints": {
|
||||||
|
"libjf:config": ["io.gitlab.jfronny.libjf.translate.impl.TranslateConfig"]
|
||||||
|
},
|
||||||
"custom": {
|
"custom": {
|
||||||
"modmenu": {
|
"modmenu": {
|
||||||
"parent": "libjf",
|
"parent": "libjf",
|
||||||
|
|
|
@ -1,24 +1,38 @@
|
||||||
package io.gitlab.jfronny.libjf.translate.test;
|
package io.gitlab.jfronny.libjf.translate.test;
|
||||||
|
|
||||||
import io.gitlab.jfronny.libjf.LibJf;
|
import io.gitlab.jfronny.libjf.LibJf;
|
||||||
import io.gitlab.jfronny.libjf.translate.api.TranslateService;
|
import io.gitlab.jfronny.libjf.translate.impl.google.GoogleTranslateService;
|
||||||
import io.gitlab.jfronny.libjf.translate.api.TranslateException;
|
import io.gitlab.jfronny.libjf.translate.impl.libretranslate.LibreTranslateService;
|
||||||
import net.fabricmc.api.ModInitializer;
|
import net.fabricmc.api.ModInitializer;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class TestEntrypoint implements ModInitializer {
|
public class TestEntrypoint implements ModInitializer {
|
||||||
@Override
|
@Override
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
try {
|
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) {
|
} catch (Throwable e) {
|
||||||
LibJf.LOGGER.error("Could not verify translation validity", e);
|
LibJf.LOGGER.error("Could not verify translation validity", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> void runTest(TranslateService<T> ts) throws TranslateException {
|
private static void assertEqual(Object o1, Object o2) {
|
||||||
final String source = "Cogito, ergo sum";
|
if (!Objects.equals(o1, o2))
|
||||||
final String expected = "I think, therefore I am";
|
throw new AssertionError("Assertion not met: expected " + o1 + " but got " + o2);
|
||||||
assert expected.equals(ts.translate(source, ts.detect(source), ts.parseLang("en")));
|
|
||||||
assert expected.equals(ts.translate(source, ts.parseLang("la"), ts.parseLang("en")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue