diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..9bc57666 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "common/src/main/resources/languages"] + path = common/src/main/resources/languages + url = https://github.com/GeyserMC/languages diff --git a/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java b/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java index 031be4ef..e6eca15c 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java +++ b/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java @@ -1,9 +1,6 @@ package org.geysermc.floodgate.api.player; -import org.geysermc.floodgate.util.DeviceOs; -import org.geysermc.floodgate.util.InputMode; -import org.geysermc.floodgate.util.LinkedPlayer; -import org.geysermc.floodgate.util.UiProfile; +import org.geysermc.floodgate.util.*; import java.util.UUID; @@ -77,6 +74,11 @@ public interface FloodgatePlayer { */ LinkedPlayer getLinkedPlayer(); + /** + * Returns the raw skin of the Bedrock player + */ + RawSkin getRawSkin(); + /** * Casts the FloodgatePlayer instance to a class that extends FloodgatePlayer. * diff --git a/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java b/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java index 3fb1cd7f..2b08163d 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java +++ b/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java @@ -50,7 +50,10 @@ public final class BungeePlugin extends Plugin { platform = injector.getInstance(FloodgatePlatform.class); long endCtm = System.currentTimeMillis(); - getLogger().info("Took " + (endCtm - ctm) + "ms to boot Floodgate"); + getLogger().info(platform.getLanguageManager().getLocaleStringLog( + "floodgate.core.finish", + endCtm - ctm + )); } @Override diff --git a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java index 61faa986..059a9eba 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java +++ b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java @@ -17,6 +17,7 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.handler.BungeeDataHandler; +import org.geysermc.floodgate.util.LanguageManager; import java.util.UUID; @@ -24,15 +25,18 @@ public final class BungeeListener implements Listener { private final BungeeDataHandler dataHandler; private final ProxyFloodgateApi api; private final FloodgateLogger logger; + private final LanguageManager languageManager; public BungeeListener(Plugin plugin, ProxyFloodgateConfig config, ProxyFloodgateApi api, HandshakeHandler handshakeHandler, - AttributeKey playerAttribute, FloodgateLogger logger) { + AttributeKey playerAttribute, FloodgateLogger logger, + LanguageManager languageManager) { this.dataHandler = new BungeeDataHandler( plugin, config, api, handshakeHandler, playerAttribute, logger ); this.api = api; this.logger = logger; + this.languageManager = languageManager; } @EventHandler(priority = EventPriority.LOW) @@ -60,8 +64,9 @@ public final class BungeeListener implements Listener { FloodgatePlayer player = api.getPlayer(uniqueId); if (player != null) { player.as(FloodgatePlayerImpl.class).setLogin(false); - logger.info("Floodgate player who is logged in as {} {} joined", - player.getCorrectUsername(), player.getCorrectUniqueId()); + logger.info(languageManager.getLocaleStringLog("floodgate.ingame.login_name", + player.getCorrectUsername(), player.getCorrectUniqueId())); + languageManager.loadFloodgateLocale(player.getLanguageCode()); } } @@ -77,9 +82,8 @@ public final class BungeeListener implements Listener { ProxiedPlayer player = event.getPlayer(); if (api.removePlayer(player.getUniqueId()) != null) { api.removeEncryptedData(player.getUniqueId()); - logger.info( - "Floodgate player who was logged in as {} {} disconnected", - player.getName(), player.getUniqueId() + logger.info(languageManager.getLocaleStringLog( + "floodgate.ingame.disconnect_name", player.getName()) ); } } diff --git a/bungee/src/main/java/org/geysermc/floodgate/module/BungeeListenerModule.java b/bungee/src/main/java/org/geysermc/floodgate/module/BungeeListenerModule.java index 66cb26bb..7431aeee 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/module/BungeeListenerModule.java +++ b/bungee/src/main/java/org/geysermc/floodgate/module/BungeeListenerModule.java @@ -40,6 +40,7 @@ import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.listener.BungeeListener; import org.geysermc.floodgate.register.ListenerRegister; +import org.geysermc.floodgate.util.LanguageManager; import javax.inject.Named; @@ -54,7 +55,9 @@ public final class BungeeListenerModule extends AbstractModule { public Listener bungeeListener(Plugin plugin, ProxyFloodgateConfig config, ProxyFloodgateApi api, HandshakeHandler handshakeHandler, @Named("playerAttribute") AttributeKey playerAttribute, - FloodgateLogger logger) { - return new BungeeListener(plugin, config, api, handshakeHandler, playerAttribute, logger); + FloodgateLogger logger, LanguageManager languageManager) { + return new BungeeListener( + plugin, config, api, handshakeHandler, playerAttribute, logger, languageManager + ); } } diff --git a/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java b/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java index 0c068cc4..bf2eb391 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java +++ b/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java @@ -49,6 +49,7 @@ import org.geysermc.floodgate.platform.command.CommandRegistration; import org.geysermc.floodgate.platform.command.util.CommandUtil; import org.geysermc.floodgate.platform.listener.ListenerRegistration; import org.geysermc.floodgate.util.BungeeCommandUtil; +import org.geysermc.floodgate.util.LanguageManager; @RequiredArgsConstructor public final class BungeePlatformModule extends AbstractModule { @@ -96,8 +97,8 @@ public final class BungeePlatformModule extends AbstractModule { @Provides @Singleton - public CommandUtil commandUtil(FloodgateLogger logger) { - return new BungeeCommandUtil(logger); + public CommandUtil commandUtil(FloodgateLogger logger, LanguageManager languageManager) { + return new BungeeCommandUtil(logger, languageManager); } @Provides diff --git a/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java b/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java index be271b5c..7fc4f905 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java +++ b/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java @@ -30,7 +30,9 @@ import lombok.RequiredArgsConstructor; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.connection.ProxiedPlayer; +import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.platform.command.CommandMessage; import org.geysermc.floodgate.platform.command.util.CommandResponseCache; import org.geysermc.floodgate.platform.command.util.CommandUtil; @@ -38,10 +40,21 @@ import org.geysermc.floodgate.platform.command.util.CommandUtil; @RequiredArgsConstructor public final class BungeeCommandUtil extends CommandResponseCache implements CommandUtil { private final FloodgateLogger logger; + private final LanguageManager manager; @Override public void sendMessage(Object player, CommandMessage message, Object... args) { - cast(player).sendMessage(getOrAddCachedMessage(message, args)); + ProxiedPlayer proxiedPlayer = cast(player); + FloodgatePlayer floodgatePlayer = + FloodgateApi.getInstance().getPlayer(proxiedPlayer.getUniqueId()); + if (floodgatePlayer != null) { + proxiedPlayer.sendMessage(transformMessage( + manager.getPlayerLocaleString(message.getMessage(), + floodgatePlayer.getLanguageCode(), args))); + } else { + proxiedPlayer.sendMessage(transformMessage( + manager.getLocaleStringLog(message.getMessage(), args))); + } } @Override diff --git a/common/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java b/common/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java index 9f7ecf6d..d646ce8e 100644 --- a/common/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java +++ b/common/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java @@ -42,6 +42,7 @@ import org.geysermc.floodgate.config.loader.ConfigLoader; import org.geysermc.floodgate.link.PlayerLinkLoader; import org.geysermc.floodgate.module.ConfigLoadedModule; import org.geysermc.floodgate.module.PostInitializeModule; +import org.geysermc.floodgate.util.LanguageManager; import java.nio.file.Files; import java.nio.file.Path; @@ -55,6 +56,9 @@ public class FloodgatePlatform { private final FloodgateLogger logger; + @Getter(AccessLevel.PROTECTED) + private final LanguageManager languageManager; + private final Injector guice; @Inject private PlatformInjector injector; @@ -63,9 +67,11 @@ public class FloodgatePlatform { public FloodgatePlatform(@Named("dataDirectory") Path dataDirectory, FloodgateApi api, ConfigLoader configLoader, PlayerLinkLoader playerLinkLoader, HandshakeHandler handshakeHandler, FloodgateLogger logger, - PlatformInjector platformInjector, Injector injector) { + PlatformInjector platformInjector, LanguageManager languageManager, + Injector injector) { this.api = api; this.logger = logger; + this.languageManager = languageManager; if (!Files.isDirectory(dataDirectory)) { try { @@ -86,6 +92,7 @@ public class FloodgatePlatform { guice.injectMembers(playerLinkLoader); guice.injectMembers(handshakeHandler); + guice.injectMembers(languageManager); PlayerLink link = playerLinkLoader.load(); diff --git a/common/src/main/java/org/geysermc/floodgate/FloodgatePlayerImpl.java b/common/src/main/java/org/geysermc/floodgate/FloodgatePlayerImpl.java index a0c16ed2..d1844030 100644 --- a/common/src/main/java/org/geysermc/floodgate/FloodgatePlayerImpl.java +++ b/common/src/main/java/org/geysermc/floodgate/FloodgatePlayerImpl.java @@ -53,6 +53,7 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer { private final InputMode inputMode; private final String ip; private final LinkedPlayer linkedPlayer; + private final RawSkin rawSkin; /** * Returns true if the player is still logging in @@ -80,6 +81,7 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer { uiProfile = UiProfile.getById(data.getUiProfile()); inputMode = InputMode.getById(data.getInputMode()); ip = data.getIp(); + rawSkin = data.getSkin(); // we'll use the LinkedPlayer provided by Bungee or Velocity (if they included one) if (data.hasPlayerLink()) { @@ -141,6 +143,6 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer { public BedrockData toBedrockData() { return new BedrockData(version, username, xuid, deviceOs.ordinal(), languageCode, - uiProfile.ordinal(), inputMode.ordinal(), ip, linkedPlayer); + uiProfile.ordinal(), inputMode.ordinal(), ip, linkedPlayer, rawSkin); } } diff --git a/common/src/main/java/org/geysermc/floodgate/command/CommonCommandMessage.java b/common/src/main/java/org/geysermc/floodgate/command/CommonCommandMessage.java index 2d86caec..b539d886 100644 --- a/common/src/main/java/org/geysermc/floodgate/command/CommonCommandMessage.java +++ b/common/src/main/java/org/geysermc/floodgate/command/CommonCommandMessage.java @@ -33,14 +33,15 @@ import org.geysermc.floodgate.platform.command.CommandMessage; * Messages (or part of messages) that are used in two or more commands and thus are 'commonly used' */ public enum CommonCommandMessage implements CommandMessage { - NOT_A_PLAYER("Please head over to your Minecraft Account and link from there."), - CHECK_CONSOLE("Please check the console for more info!"), - IS_LINKED_ERROR("&cError while checking if the given player is linked. " + CHECK_CONSOLE); + NOT_A_PLAYER("floodgate.commands.not_a_player"), + CHECK_CONSOLE("floodgate.commands.check_console"), + // TODO used to also have console check + IS_LINKED_ERROR("floodgate.commands.is_linked_error"); @Getter private final String message; CommonCommandMessage(String message) { - this.message = message.replace('&', COLOR_CHAR); + this.message = message; } @Override diff --git a/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java b/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java index 6a7d57dd..0985613b 100644 --- a/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java +++ b/common/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java @@ -146,26 +146,22 @@ public final class LinkAccountCommand implements Command { } public enum Message implements CommandMessage { - ALREADY_LINKED("&cYour account is already linked!\n" + - "&cIf you want to link to a different account, run &6/unlinkaccount&c and try it again." - ), - JAVA_USAGE("&cUsage: /linkaccount "), - LINK_REQUEST_CREATED("&aLog in as {} on Bedrock and run &6/linkaccount {} {}\n" + - "&cWarning: Any progress on your Bedrock account will not be carried over! Save any items in your inventory first.\n" + - "&cIf you change your mind you can run &6/unlinkaccount&c to get your progess back." - ), - BEDROCK_USAGE("&cStart the process from Java! Usage: /linkaccount "), - LINK_REQUEST_EXPIRED("&cThe code you entered is expired! Run &6/linkaccount&c again on your Java account"), - LINK_REQUEST_COMPLETED("You are successfully linked to {}!\nIf you want to undo this run /unlinkaccount"), - LINK_REQUEST_ERROR("&cAn error occurred while linking. " + CommonCommandMessage.CHECK_CONSOLE), - INVALID_CODE("&cInvalid code! Please check your code or run the &6/linkaccount&c command again on your Java account."), - NO_LINK_REQUESTED("&cThis player has not requested an account link! Please log in on Java and request one with &6/linkaccount"), - LINK_REQUEST_DISABLED("&cLinking is not enabled on this server."); + ALREADY_LINKED("floodgate.command.link_account.already_linked"), + JAVA_USAGE("floodgate.command.link_account.java_usage"), + LINK_REQUEST_CREATED("floodgate.command.link_account.link_request_created"), + BEDROCK_USAGE("floodgate.command.link_account.bedrock_usage"), + LINK_REQUEST_EXPIRED("floodgate.command.link_account.link_request_expired"), + LINK_REQUEST_COMPLETED("floodgate.command.link_account.link_request_completed"), + // TODO this also used to have another message + LINK_REQUEST_ERROR("floodgate.command.link_request.error"), + INVALID_CODE("floodgate.command.link_account.invalid_code"), + NO_LINK_REQUESTED("floodgate.command.link_account.no_link_requested"), + LINK_REQUEST_DISABLED("floodgate.commands.linking_disabled"); @Getter private final String message; Message(String message) { - this.message = message.replace('&', COLOR_CHAR); + this.message = message; } } } diff --git a/common/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java b/common/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java index 9699edef..de538186 100644 --- a/common/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java +++ b/common/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java @@ -90,15 +90,16 @@ public final class UnlinkAccountCommand implements Command { } public enum Message implements CommandMessage { - NOT_LINKED("&cYour account isn't linked"), - UNLINK_SUCCESS("&cUnlink successful! Rejoin to return to your Bedrock account"), - UNLINK_ERROR("&cAn error occurred while unlinking player! " + CommonCommandMessage.CHECK_CONSOLE), - LINKING_NOT_ENABLED("&cLinking is not enabled on this server"); + NOT_LINKED("floodgate.command.unlink_account.not_linked"), + UNLINK_SUCCESS("floodgate.command.unlink_account.unlink_success"), + // TODO also used to have CHECK_CONSOLE + UNLINK_ERROR("floodgate.command.unlink_account.error"), + LINKING_NOT_ENABLED("floodgate.commands.linking_disabled"); @Getter private final String message; Message(String message) { - this.message = message.replace('&', COLOR_CHAR); + this.message = message; } } } diff --git a/common/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java b/common/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java index b0dc46c3..a4bbf0b4 100644 --- a/common/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java +++ b/common/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java @@ -46,6 +46,9 @@ public class FloodgateConfig { @JsonProperty(value = "replace-spaces") private boolean replaceSpaces; + @JsonProperty(value = "default-locale") + private String defaultLocale; + @JsonProperty(value = "disconnect") private DisconnectMessages messages; diff --git a/common/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java b/common/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java index 0347231b..d7f2f671 100644 --- a/common/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java +++ b/common/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java @@ -37,6 +37,8 @@ import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.crypto.KeyProducer; import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -74,7 +76,13 @@ public class ConfigLoader { boolean newConfig = !Files.exists(configPath); try { if (newConfig) { - Files.copy(defaultConfigPath, configPath); + InputStream newConfigFile = + ConfigLoader.class.getClassLoader().getResourceAsStream(defaultConfigName); + if (newConfigFile == null) { + throw new RuntimeException("Failed to get the default config file!"); + } + + Files.copy(newConfigFile, configPath); Key key = keyProducer.produce(); cipher.init(key); diff --git a/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java b/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java index 503e92d4..1da43b75 100644 --- a/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java +++ b/common/src/main/java/org/geysermc/floodgate/config/updater/ConfigUpdater.java @@ -45,6 +45,8 @@ public class ConfigUpdater { private final ConfigFileUpdater fileUpdater; private final FloodgateLogger logger; + private static final int CONFIG_VERSION = 1; + public void update(Path defaultConfigLocation) { Path configLocation = dataFolder.resolve("config.yml"); @@ -70,12 +72,12 @@ public class ConfigUpdater { int version = (int) versionElement; checkArgument( - version == 1, - "Config is newer then possible on this version! Expected 1, got " + version + version == CONFIG_VERSION, + "Config is newer then possible on this version! Expected " + CONFIG_VERSION + ", got " + version ); // config is already up-to-date - if (version == 1) { + if (version == CONFIG_VERSION) { return; } } else { diff --git a/common/src/main/java/org/geysermc/floodgate/module/CommonModule.java b/common/src/main/java/org/geysermc/floodgate/module/CommonModule.java index ddc3e9b7..cf0ac1b6 100644 --- a/common/src/main/java/org/geysermc/floodgate/module/CommonModule.java +++ b/common/src/main/java/org/geysermc/floodgate/module/CommonModule.java @@ -45,6 +45,7 @@ import org.geysermc.floodgate.config.updater.ConfigUpdater; import org.geysermc.floodgate.crypto.*; import org.geysermc.floodgate.inject.CommonPlatformInjector; import org.geysermc.floodgate.link.PlayerLinkLoader; +import org.geysermc.floodgate.util.LanguageManager; import java.nio.file.Path; @@ -94,6 +95,12 @@ public final class CommonModule extends AbstractModule { return new ConfigUpdater(dataDirectory, configFileUpdater, logger); } + @Provides + @Singleton + public LanguageManager languageLoader(FloodgateLogger logger) { + return new LanguageManager(logger); + } + @Provides @Singleton public PlayerLinkLoader playerLinkLoader() { diff --git a/common/src/main/java/org/geysermc/floodgate/util/LanguageManager.java b/common/src/main/java/org/geysermc/floodgate/util/LanguageManager.java new file mode 100644 index 00000000..31be1dbf --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/LanguageManager.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2019-2020 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + * + */ + +package org.geysermc.floodgate.util; + +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.config.FloodgateConfig; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +/** + * Manages translations for strings in Floodgate + */ +public class LanguageManager { + private final Map LOCALE_MAPPINGS = new HashMap<>(); + + private final FloodgateLogger logger; + + /** + * The locale used in console and as a fallback + */ + private String defaultLocale; + + public LanguageManager(FloodgateLogger logger) { + this.logger = logger; + } + + /** + * Loads the log's locale file once Floodgate loads the config + * + * @param config the Floodgate config + */ + public void initialize(FloodgateConfig config) { + loadFloodgateLocale("en_US"); // Fallback + + if (config.getDefaultLocale() != null && + isValidLanguage(formatLocale(config.getDefaultLocale()))) { + loadFloodgateLocale(formatLocale(config.getDefaultLocale())); + defaultLocale = formatLocale(config.getDefaultLocale()); + } else { + String systemLocale = formatLocale(Locale.getDefault().getLanguage() + "_" + + Locale.getDefault().getCountry()); + if (isValidLanguage(systemLocale)) { + loadFloodgateLocale(systemLocale); + defaultLocale = systemLocale; + } else { + defaultLocale = "en_US"; + } + } + } + + /** + * Loads a Floodgate locale from resources; if the file doesn't exist it just logs a warning + * + * @param locale Locale to load + */ + public void loadFloodgateLocale(String locale) { + locale = formatLocale(locale); + + InputStream localeStream = LanguageManager.class.getClassLoader().getResourceAsStream( + "languages/texts/" + locale + ".properties"); + + // Load the locale + if (localeStream != null) { + Properties localeProp = new Properties(); + try { + localeProp.load(new InputStreamReader(localeStream, StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new AssertionError("Failed to load Floodgate locale", e); + } + + // Insert the locale into the mappings + LOCALE_MAPPINGS.put(locale, localeProp); + } else { + logger.warn("Missing locale file: " + locale); + } + } + + /** + * Get a formatted language string with the default locale for Floodgate + * + * @param key Language string to translate + * @param values Values to put into the string + * @return Translated string or the original message if it was not found in the given locale + */ + public String getLocaleStringLog(String key, Object... values) { + return getPlayerLocaleString(key, defaultLocale, values); + } + + /** + * Get a formatted language string with the given locale for Floodgate + * + * @param key Language string to translate + * @param locale Locale to translate to + * @param values Values to put into the string + * @return Translated string or the original message if it was not found in the given locale + */ + public String getPlayerLocaleString(String key, String locale, Object... values) { + locale = formatLocale(locale); + + Properties properties = LOCALE_MAPPINGS.get(locale); + String formatString = properties.getProperty(key); + + // Try and get the key from the default locale + if (formatString == null) { + properties = LOCALE_MAPPINGS.get(defaultLocale); + formatString = properties.getProperty(key); + } + + // Try and get the key from en_US (this should only ever happen in development) + if (formatString == null) { + properties = LOCALE_MAPPINGS.get("en_US"); + formatString = properties.getProperty(key); + } + + // Final fallback + if (formatString == null) { + formatString = key; + } + + return MessageFormat.format(formatString.replace("'", "''").replace("&", "\u00a7"), values); + } + + /** + * Cleans up and formats a locale string + * + * @param locale The locale to format + * @return The formatted locale + */ + private static String formatLocale(String locale) { + try { + String[] parts = locale.toLowerCase().split("_"); + return parts[0] + "_" + parts[1].toUpperCase(); + } catch (Exception e) { + return locale; + } + } + + /** + * Ensures that the given locale is supported by Floodgate + * @param locale the locale to validate + * @return true if the given locale is supported by Floodgate + */ + private boolean isValidLanguage(String locale) { + boolean result = true; + if (LanguageManager.class.getResource("/languages/texts/" + locale + ".properties") == null) { + result = false; + logger.warn(locale + " is not a supported Floodgate language."); + } else { + if (!LOCALE_MAPPINGS.containsKey(locale)) { + loadFloodgateLocale(locale); + } + } + return result; + } +} diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index 9cb41687..d5ba317b 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -11,6 +11,9 @@ username-prefix: "*" # Should spaces be replaced with '_' in bedrock usernames? replace-spaces: true +# The default locale for Floodgate. By default, Floodgate uses the system locale +# default-locale: en_US + disconnect: # The disconnect message Geyser users should get when connecting # to the server with an invalid key diff --git a/common/src/main/resources/languages b/common/src/main/resources/languages new file mode 160000 index 00000000..8826fda0 --- /dev/null +++ b/common/src/main/resources/languages @@ -0,0 +1 @@ +Subproject commit 8826fda0eaeaa1cf70e212ca2dcf74a51eb32083 diff --git a/common/src/main/resources/proxy-config.yml b/common/src/main/resources/proxy-config.yml index 49e55be8..af3a80e5 100644 --- a/common/src/main/resources/proxy-config.yml +++ b/common/src/main/resources/proxy-config.yml @@ -11,6 +11,9 @@ username-prefix: "*" # Should spaces be replaced with '_' in bedrock usernames? replace-spaces: true +# The default locale for Floodgate. By default, Floodgate uses the system locale +# default-locale: en_US + # Should the proxy send the bedrock player data to the servers it is connecting to? # This requires Floodgate to be installed on the servers. # You'll get kicked if you don't use the plugin. The default value is false because of it diff --git a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java b/spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java index 885dca85..4c41ee2f 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java +++ b/spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java @@ -37,6 +37,7 @@ import org.geysermc.floodgate.api.inject.PlatformInjector; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.loader.ConfigLoader; import org.geysermc.floodgate.link.PlayerLinkLoader; +import org.geysermc.floodgate.util.LanguageManager; import java.nio.file.Path; @@ -47,9 +48,10 @@ public final class SpigotPlatform extends FloodgatePlatform { public SpigotPlatform(@Named("dataDirectory") Path dataDirectory, FloodgateApi api, ConfigLoader configLoader, PlayerLinkLoader playerLinkLoader, HandshakeHandler handshakeHandler, FloodgateLogger logger, - PlatformInjector platformInjector, Injector injector) { - super(dataDirectory, api, configLoader, playerLinkLoader, - handshakeHandler, logger, platformInjector, injector); + PlatformInjector platformInjector, LanguageManager languageManager, + Injector injector) { + super(dataDirectory, api, configLoader, playerLinkLoader, handshakeHandler, + logger, platformInjector, languageManager, injector); } @Override diff --git a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java b/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java index 510b1ec4..2a0ce858 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java +++ b/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java @@ -49,7 +49,10 @@ public final class SpigotPlugin extends JavaPlugin { platform = injector.getInstance(SpigotPlatform.class); long endCtm = System.currentTimeMillis(); - getLogger().info("Took " + (endCtm - ctm) + "ms to boot Floodgate"); + getLogger().info(platform.getLanguageManager().getLocaleStringLog( + "floodgate.core.finish", + endCtm - ctm + )); } @Override diff --git a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java index 6d1b1256..6ee50047 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java +++ b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java @@ -38,6 +38,7 @@ import org.geysermc.floodgate.FloodgatePlayerImpl; import org.geysermc.floodgate.api.SimpleFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.util.LanguageManager; import java.util.UUID; @@ -45,6 +46,7 @@ import java.util.UUID; public final class SpigotListener implements Listener { private final SimpleFloodgateApi api; private final FloodgateLogger logger; + private final LanguageManager languageManager; @EventHandler(priority = EventPriority.MONITOR) public void onAsyncPreLogin(AsyncPlayerPreLoginEvent event) { @@ -66,8 +68,9 @@ public final class SpigotListener implements Listener { FloodgatePlayer player = api.getPlayer(uniqueId); if (player != null) { player.as(FloodgatePlayerImpl.class).setLogin(false); - logger.info("Floodgate player who is logged in as {} {} joined", - player.getCorrectUsername(), player.getCorrectUniqueId()); + logger.info(languageManager.getLocaleStringLog("floodgate.ingame.login_name", + player.getCorrectUsername(), player.getCorrectUniqueId())); + languageManager.loadFloodgateLocale(player.getLanguageCode()); } } @@ -75,9 +78,8 @@ public final class SpigotListener implements Listener { public void onPlayerQuit(PlayerQuitEvent event) { Player player = event.getPlayer(); if (api.removePlayer(player.getUniqueId()) != null) { - logger.info( - "Floodgate player who was logged in as {} {} disconnected", - player.getName(), player.getUniqueId() + logger.info(languageManager.getLocaleStringLog( + "floodgate.ingame.disconnect_name", player.getName()) ); } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotListenerModule.java b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotListenerModule.java index 7cd0b095..2c92d58c 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotListenerModule.java +++ b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotListenerModule.java @@ -35,6 +35,7 @@ import org.geysermc.floodgate.api.SimpleFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.listener.SpigotListener; import org.geysermc.floodgate.register.ListenerRegister; +import org.geysermc.floodgate.util.LanguageManager; public final class SpigotListenerModule extends AbstractModule { @Override @@ -44,7 +45,8 @@ public final class SpigotListenerModule extends AbstractModule { @Singleton @ProvidesIntoSet - public Listener spigotListener(SimpleFloodgateApi api, FloodgateLogger logger) { - return new SpigotListener(api, logger); + public Listener spigotListener(SimpleFloodgateApi api, FloodgateLogger logger, + LanguageManager languageManager) { + return new SpigotListener(api, logger, languageManager); } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java index 4c2853ba..a1efd8ca 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java +++ b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java @@ -45,6 +45,7 @@ import org.geysermc.floodgate.logger.JavaUtilFloodgateLogger; import org.geysermc.floodgate.platform.command.CommandRegistration; import org.geysermc.floodgate.platform.listener.ListenerRegistration; import org.geysermc.floodgate.platform.command.util.CommandUtil; +import org.geysermc.floodgate.util.LanguageManager; import org.geysermc.floodgate.util.SpigotCommandUtil; @RequiredArgsConstructor @@ -88,8 +89,8 @@ public final class SpigotPlatformModule extends AbstractModule { @Provides @Singleton - public CommandUtil commandUtil(FloodgateLogger logger) { - return new SpigotCommandUtil(plugin, logger); + public CommandUtil commandUtil(FloodgateLogger logger, LanguageManager languageManager) { + return new SpigotCommandUtil(plugin, logger, languageManager); } @Provides diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java index f8833805..4a399210 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java @@ -30,7 +30,9 @@ import lombok.RequiredArgsConstructor; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; +import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.platform.command.CommandMessage; import org.geysermc.floodgate.platform.command.util.CommandResponseCache; import org.geysermc.floodgate.platform.command.util.CommandUtil; @@ -39,10 +41,19 @@ import org.geysermc.floodgate.platform.command.util.CommandUtil; public final class SpigotCommandUtil extends CommandResponseCache implements CommandUtil { private final JavaPlugin plugin; private final FloodgateLogger logger; + private final LanguageManager manager; @Override public void sendMessage(Object player, CommandMessage message, Object... args) { - cast(player).sendMessage(format(message, args)); + Player bukkitPlayer = cast(player); + FloodgatePlayer floodgatePlayer = + FloodgateApi.getInstance().getPlayer(bukkitPlayer.getUniqueId()); + if (floodgatePlayer != null) { + bukkitPlayer.sendMessage(manager.getPlayerLocaleString(message.getMessage(), + floodgatePlayer.getLanguageCode(), args)); + } else { + bukkitPlayer.sendMessage(manager.getLocaleStringLog(message.getMessage(), args)); + } } @Override diff --git a/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java b/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java index 945c9cd5..deca5743 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java +++ b/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java @@ -53,7 +53,10 @@ public final class VelocityPlugin { platform = injector.getInstance(FloodgatePlatform.class); long endCtm = System.currentTimeMillis(); - logger.info("Took " + (endCtm - ctm) + "ms to boot Floodgate"); + logger.info(platform.getLanguageManager().getLocaleStringLog( + "floodgate.core.finish", + endCtm - ctm + )); } @Subscribe diff --git a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java index 9c316f17..9476ffad 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java +++ b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java @@ -31,6 +31,7 @@ import com.google.common.cache.CacheBuilder; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; +import com.velocitypowered.api.event.connection.LoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.proxy.InboundConnection; @@ -42,6 +43,7 @@ import net.kyori.adventure.text.TextComponent; import org.geysermc.floodgate.api.ProxyFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.util.LanguageManager; import java.lang.reflect.Field; import java.util.ArrayList; @@ -58,17 +60,19 @@ public final class VelocityListener { private final AttributeKey playerAttribute; private final AttributeKey kickMessageAttribute; private final FloodgateLogger logger; + private final LanguageManager languageManager; private final Cache playerCache; public VelocityListener(ProxyFloodgateApi api, AttributeKey playerAttribute, AttributeKey kickMessageAttribute, - FloodgateLogger logger) { + FloodgateLogger logger, LanguageManager languageManager) { this.api = api; this.playerAttribute = playerAttribute; this.kickMessageAttribute = kickMessageAttribute; this.logger = logger; + this.languageManager = languageManager; this.playerCache = CacheBuilder.newBuilder() .maximumSize(500) @@ -112,6 +116,14 @@ public final class VelocityListener { } } + @Subscribe + public void onLogin(LoginEvent event) { + FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId()); + if (player != null) { + languageManager.loadFloodgateLocale(player.getLanguageCode()); + } + } + @Subscribe(order = PostOrder.LAST) public void onDisconnect(DisconnectEvent event) { Player player = event.getPlayer(); @@ -122,8 +134,9 @@ public final class VelocityListener { if (fPlayer != null && api.removePlayer(fPlayer)) { api.removeEncryptedData(event.getPlayer().getUniqueId()); - logger.info("Floodgate player who was logged in as {} {} disconnected", - player.getUsername(), player.getUniqueId()); + logger.info(languageManager.getLocaleStringLog( + "floodgate.ingame.disconnect_name", player.getUsername() + )); } } catch (Exception exception) { logger.error("Failed to remove the player", exception); diff --git a/velocity/src/main/java/org/geysermc/floodgate/module/VelocityListenerModule.java b/velocity/src/main/java/org/geysermc/floodgate/module/VelocityListenerModule.java index 6d3ca869..a35f2b19 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/module/VelocityListenerModule.java +++ b/velocity/src/main/java/org/geysermc/floodgate/module/VelocityListenerModule.java @@ -35,6 +35,7 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.listener.VelocityListener; import org.geysermc.floodgate.register.ListenerRegister; +import org.geysermc.floodgate.util.LanguageManager; public final class VelocityListenerModule extends AbstractModule { @Override @@ -45,8 +46,9 @@ public final class VelocityListenerModule extends AbstractModule { @Singleton @ProvidesIntoSet public Object velocityListener(ProxyFloodgateApi api, FloodgateLogger logger, + LanguageManager languageManager, @Named("playerAttribute") AttributeKey playerAttr, @Named("kickMessageAttribute") AttributeKey kickMessageAttr) { - return new VelocityListener(api, playerAttr, kickMessageAttr, logger); + return new VelocityListener(api, playerAttr, kickMessageAttr, logger, languageManager); } } diff --git a/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java b/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java index 86d7a4fb..fde715fb 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java +++ b/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java @@ -49,6 +49,7 @@ import org.geysermc.floodgate.logger.Slf4jFloodgateLogger; import org.geysermc.floodgate.platform.command.CommandRegistration; import org.geysermc.floodgate.platform.command.util.CommandUtil; import org.geysermc.floodgate.platform.listener.ListenerRegistration; +import org.geysermc.floodgate.util.LanguageManager; import org.geysermc.floodgate.util.VelocityCommandUtil; import org.slf4j.Logger; @@ -91,8 +92,9 @@ public final class VelocityPlatformModule extends AbstractModule { @Provides @Singleton - public VelocityCommandUtil commandUtil(FloodgateLogger logger) { - return new VelocityCommandUtil(logger); + public VelocityCommandUtil commandUtil(FloodgateLogger logger, + LanguageManager languageManager) { + return new VelocityCommandUtil(logger, languageManager); } @Provides diff --git a/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java b/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java index b396a91a..2e801363 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java +++ b/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java @@ -30,7 +30,9 @@ import com.velocitypowered.api.proxy.Player; import lombok.RequiredArgsConstructor; import net.kyori.text.TextComponent; import net.kyori.text.serializer.legacy.LegacyComponentSerializer; +import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.platform.command.CommandMessage; import org.geysermc.floodgate.platform.command.util.CommandResponseCache; import org.geysermc.floodgate.platform.command.util.CommandUtil; @@ -38,10 +40,21 @@ import org.geysermc.floodgate.platform.command.util.CommandUtil; @RequiredArgsConstructor public final class VelocityCommandUtil extends CommandResponseCache implements CommandUtil { private final FloodgateLogger logger; + private final LanguageManager manager; @Override public void sendMessage(Object player, CommandMessage message, Object... args) { - cast(player).sendMessage(getOrAddCachedMessage(message, args)); + Player velocityPlayer = cast(player); + FloodgatePlayer floodgatePlayer = + FloodgateApi.getInstance().getPlayer(velocityPlayer.getUniqueId()); + if (floodgatePlayer != null) { + velocityPlayer.sendMessage( + transformMessage(manager.getPlayerLocaleString(message.getMessage(), + floodgatePlayer.getLanguageCode(), args))); + } else { + velocityPlayer.sendMessage( + transformMessage(manager.getLocaleStringLog(message.getMessage(), args))); + } } @Override