From 8a673875a6f4fb99b707627c1b981fd5a6d5b39f Mon Sep 17 00:00:00 2001 From: Vankka Date: Wed, 13 Apr 2022 14:07:43 +0300 Subject: [PATCH] Translations --- .../discordsrv/bukkit/BukkitDiscordSRV.java | 60 ++++++++++++++++++- common/build.gradle | 2 +- .../common/component/ComponentFactory.java | 30 +++++++++- .../translation/StringFormatTranslation.java | 33 ++++++++++ .../component/translation/Translation.java | 29 +++++++++ .../translation/TranslationRegistry.java | 55 +++++++++++++++++ .../common/discord/api/DiscordAPIImpl.java | 9 +-- 7 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 common/src/main/java/com/discordsrv/common/component/translation/StringFormatTranslation.java create mode 100644 common/src/main/java/com/discordsrv/common/component/translation/Translation.java create mode 100644 common/src/main/java/com/discordsrv/common/component/translation/TranslationRegistry.java diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java index 38d30b86..48c9e468 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/BukkitDiscordSRV.java @@ -33,6 +33,7 @@ import com.discordsrv.bukkit.player.BukkitPlayerProvider; import com.discordsrv.bukkit.plugin.BukkitPluginManager; import com.discordsrv.bukkit.scheduler.BukkitScheduler; import com.discordsrv.common.command.game.handler.ICommandHandler; +import com.discordsrv.common.component.translation.Translation; import com.discordsrv.common.config.manager.ConnectionConfigManager; import com.discordsrv.common.config.manager.MainConfigManager; import com.discordsrv.common.debug.data.OnlineMode; @@ -40,6 +41,7 @@ import com.discordsrv.common.logging.Logger; import com.discordsrv.common.messageforwarding.game.MinecraftToDiscordChatModule; import com.discordsrv.common.plugin.PluginManager; import com.discordsrv.common.server.ServerDiscordSRV; +import com.fasterxml.jackson.databind.JsonNode; import dev.vankka.dependencydownload.classpath.ClasspathAppender; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import org.bukkit.Server; @@ -47,8 +49,14 @@ import org.bukkit.plugin.ServicePriority; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; +import java.io.InputStream; import java.lang.reflect.Field; +import java.net.URL; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; public class BukkitDiscordSRV extends ServerDiscordSRV { @@ -169,13 +177,63 @@ public class BukkitDiscordSRV extends ServerDiscordSRV translations = new HashMap<>(); + try { + URL enUS = findResource("assets/minecraft/lang/en_US.lang"); + if (enUS == null) { + enUS = findResource("assets/minecraft/lang/en_us.lang"); + } + if (enUS != null) { + Properties properties = new Properties(); + try (InputStream inputStream = enUS.openStream()) { + properties.load(inputStream); + } + + properties.forEach((k, v) -> translations.put((String) k, Translation.stringFormat((String) v))); + } + } catch (Throwable t) { + logger().debug("Failed to load locale", t); + } + try { + URL enUS = findResource("assets/minecraft/lang/en_us.json"); + if (enUS != null) { + JsonNode node = json().readTree(enUS); + node.fields().forEachRemaining(entry -> translations.put( + entry.getKey(), + Translation.stringFormat(entry.getValue().textValue())) + ); + } + } catch (Throwable t) { + logger().debug("Failed to load locale", t); + } + + if (translations.isEmpty()) { + logger().warning("No Minecraft translations were found, some components may not render correctly"); + } else { + componentFactory().translationRegistry().register(Locale.US, translations); + logger().debug("Found " + translations.size() + " Minecraft translations for en_us"); + } + } + @Override protected void enable() throws Throwable { // Service provider server().getServicesManager().register(DiscordSRVApi.class, this, plugin(), ServicePriority.Normal); - // Adventure audiences + // Adventure related stuff this.audiences = BukkitAudiences.create(bootstrap.getPlugin()); + loadMCTranslations(); // Command handler commandHandler = AbstractBukkitCommandHandler.get(this); diff --git a/common/build.gradle b/common/build.gradle index a4b61673..f78d8ef9 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -52,7 +52,7 @@ dependencies { runtimeDownloadApi 'org.spongepowered:configurate-hocon:' + rootProject.configurateVersion // Jackson (transitive in :api) - compileOnly 'com.fasterxml.jackson.core:jackson-databind:2.13.1' + compileOnlyApi 'com.fasterxml.jackson.core:jackson-databind:2.10.1' // Logging compileOnlyApi project(':common:common-slf4j-hack') diff --git a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java index eccb4bb5..1eb45f03 100644 --- a/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java +++ b/common/src/main/java/com/discordsrv/common/component/ComponentFactory.java @@ -23,25 +23,49 @@ import com.discordsrv.api.component.MinecraftComponent; import com.discordsrv.api.component.MinecraftComponentFactory; import com.discordsrv.common.DiscordSRV; import com.discordsrv.common.component.renderer.DiscordSRVMinecraftRenderer; +import com.discordsrv.common.component.translation.Translation; +import com.discordsrv.common.component.translation.TranslationRegistry; import dev.vankka.mcdiscordreserializer.discord.DiscordSerializer; import dev.vankka.mcdiscordreserializer.discord.DiscordSerializerOptions; import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializer; import dev.vankka.mcdiscordreserializer.minecraft.MinecraftSerializerOptions; import org.jetbrains.annotations.NotNull; +import java.util.Locale; + public class ComponentFactory implements MinecraftComponentFactory { private final DiscordSRV discordSRV; private final MinecraftSerializer minecraftSerializer; private final DiscordSerializer discordSerializer; + // Not the same as Adventure's TranslationRegistry + private final TranslationRegistry translationRegistry = new TranslationRegistry(); + public ComponentFactory(DiscordSRV discordSRV) { this.discordSRV = discordSRV; this.minecraftSerializer = new MinecraftSerializer( MinecraftSerializerOptions.defaults() .addRenderer(new DiscordSRVMinecraftRenderer(discordSRV)) ); - this.discordSerializer = new DiscordSerializer(DiscordSerializerOptions.defaults()); + this.discordSerializer = new DiscordSerializer(); + discordSerializer.setDefaultOptions( + DiscordSerializerOptions.defaults() + .withTranslationProvider(translatableComponent -> { + Translation translation = translationRegistry.lookup(Locale.US, translatableComponent.key()); + if (translation == null) { + return null; + } + + return translation.translate( + translatableComponent.args() + .stream() + .map(discordSerializer::serialize) + .map(str -> (Object) str) + .toArray(Object[]::new) + ); + }) + ); } @Override @@ -61,4 +85,8 @@ public class ComponentFactory implements MinecraftComponentFactory { public DiscordSerializer discordSerializer() { return discordSerializer; } + + public TranslationRegistry translationRegistry() { + return translationRegistry; + } } diff --git a/common/src/main/java/com/discordsrv/common/component/translation/StringFormatTranslation.java b/common/src/main/java/com/discordsrv/common/component/translation/StringFormatTranslation.java new file mode 100644 index 00000000..e3563679 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/component/translation/StringFormatTranslation.java @@ -0,0 +1,33 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.component.translation; + +public class StringFormatTranslation implements Translation { + + private final String format; + + public StringFormatTranslation(String format) { + this.format = format; + } + + @Override + public String translate(Object[] arguments) { + return String.format(format, arguments); + } +} diff --git a/common/src/main/java/com/discordsrv/common/component/translation/Translation.java b/common/src/main/java/com/discordsrv/common/component/translation/Translation.java new file mode 100644 index 00000000..8bd42670 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/component/translation/Translation.java @@ -0,0 +1,29 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.component.translation; + +@FunctionalInterface +public interface Translation { + + static Translation stringFormat(String format) { + return new StringFormatTranslation(format); + } + + String translate(Object[] arguments); +} diff --git a/common/src/main/java/com/discordsrv/common/component/translation/TranslationRegistry.java b/common/src/main/java/com/discordsrv/common/component/translation/TranslationRegistry.java new file mode 100644 index 00000000..f3e20e6d --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/component/translation/TranslationRegistry.java @@ -0,0 +1,55 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2022 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.component.translation; + +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * Our own simple class that stores abstract {@link Translation}s instead of MessageFormats, + * to be more flexible, and support Minecraft's own language files. + */ +public class TranslationRegistry { + + private static final Locale DEFAULT_LOCALE = Locale.US; + + private final Map> translations = new LinkedHashMap<>(); + + public TranslationRegistry() {} + + public void register(Locale locale, Map translations) { + synchronized (this.translations) { + this.translations.computeIfAbsent(locale, key -> new LinkedHashMap<>()).putAll(translations); + } + } + + @Nullable + public Translation lookup(Locale locale, String key) { + synchronized (translations) { + return translations.getOrDefault( + locale, // Try the suggested locale first + translations.getOrDefault( + DEFAULT_LOCALE, // Then try the default locale + Collections.emptyMap() // Then fail + ) + ).get(key); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java index 62529e59..66516d6e 100644 --- a/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java +++ b/common/src/main/java/com/discordsrv/common/discord/api/DiscordAPIImpl.java @@ -307,6 +307,7 @@ public class DiscordAPIImpl implements DiscordAPI { try { return mapExceptions(futureSupplier.get()); } catch (Throwable t) { + t.printStackTrace(); CompletableFuture future = new CompletableFuture<>(); future.completeExceptionally(t); return future; @@ -314,16 +315,16 @@ public class DiscordAPIImpl implements DiscordAPI { } public CompletableFuture mapExceptions(CompletableFuture future) { - return future.handle((msg, t) -> { + return future.handle((response, t) -> { if (t instanceof ErrorResponseException) { ErrorResponseException exception = (ErrorResponseException) t; int code = exception.getErrorCode(); - ErrorResponse response = exception.getErrorResponse(); - throw new RestErrorResponseException(code, response != null ? response.getMeaning() : "Unknown", t); + ErrorResponse errorResponse = exception.getErrorResponse(); + throw new RestErrorResponseException(code, errorResponse != null ? errorResponse.getMeaning() : "Unknown", t); } else if (t != null) { throw (RuntimeException) t; } - return msg; + return response; }); }