diff --git a/bukkit/src/main/resources/config.yml b/bukkit/src/main/resources/config.yml index e4a924789..d51a2893b 100644 --- a/bukkit/src/main/resources/config.yml +++ b/bukkit/src/main/resources/config.yml @@ -310,6 +310,9 @@ log-notify: true log-notify-filtered-descriptions: # - "parent add example" +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + # Defines the options for prefix and suffix stacking. # # - The feature allows you to display multiple prefixes or suffixes alongside a players username in diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index b2c291a7b..1c1d4e387 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -318,6 +318,9 @@ log-notify: true log-notify-filtered-descriptions: # - "parent add example" +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + # Defines the options for prefix and suffix stacking. # # - The feature allows you to display multiple prefixes or suffixes alongside a players username in diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java index 8a39dd5eb..f0654c25f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/misc/TranslationsCommand.java @@ -25,9 +25,6 @@ package me.lucko.luckperms.common.commands.misc; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - import me.lucko.luckperms.common.command.CommandResult; import me.lucko.luckperms.common.command.abstraction.SingleCommand; import me.lucko.luckperms.common.command.access.CommandPermission; @@ -36,36 +33,19 @@ import me.lucko.luckperms.common.command.utils.ArgumentList; import me.lucko.luckperms.common.http.UnsuccessfulRequestException; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.locale.TranslationRepository.LanguageInfo; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.sender.Sender; -import me.lucko.luckperms.common.util.MoreFiles; import me.lucko.luckperms.common.util.Predicates; -import me.lucko.luckperms.common.util.gson.GsonProvider; import net.kyori.adventure.text.Component; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.util.ArrayList; import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; public class TranslationsCommand extends SingleCommand { - private static final String TRANSLATIONS_INFO_ENDPOINT = "https://metadata.luckperms.net/data/translations"; - private static final String TRANSLATIONS_DOWNLOAD_ENDPOINT = "https://metadata.luckperms.net/translation/"; public TranslationsCommand() { super(CommandSpec.TRANSLATIONS, "Translations", CommandPermission.TRANSLATIONS, Predicates.notInRange(0, 1)); @@ -77,7 +57,7 @@ public class TranslationsCommand extends SingleCommand { List availableTranslations; try { - availableTranslations = getAvailableTranslations(plugin); + availableTranslations = plugin.getTranslationRepository().getAvailableLanguages(); } catch (IOException | UnsuccessfulRequestException e) { Message.TRANSLATIONS_SEARCHING_ERROR.send(sender); plugin.getLogger().warn("Unable to obtain a list of available translations", e); @@ -86,10 +66,7 @@ public class TranslationsCommand extends SingleCommand { if (args.size() >= 1 && args.get(0).equalsIgnoreCase("install")) { Message.TRANSLATIONS_INSTALLING.send(sender); - - downloadTranslations(plugin, availableTranslations, sender); - plugin.getTranslationManager().reload(); - + plugin.getTranslationRepository().downloadAndInstallTranslations(availableTranslations, sender, true); Message.TRANSLATIONS_INSTALL_COMPLETE.send(sender); return CommandResult.SUCCESS; } @@ -98,109 +75,11 @@ public class TranslationsCommand extends SingleCommand { Message.AVAILABLE_TRANSLATIONS_HEADER.send(sender); for (LanguageInfo language : availableTranslations) { - Message.AVAILABLE_TRANSLATIONS_ENTRY.send(sender, language.locale.toString(), localeDisplayName(language.locale), language.progress, language.contributors); + Message.AVAILABLE_TRANSLATIONS_ENTRY.send(sender, language.locale().toString(), TranslationManager.localeDisplayName(language.locale()), language.progress(), language.contributors()); } sender.sendMessage(Message.prefixed(Component.empty())); Message.TRANSLATIONS_DOWNLOAD_PROMPT.send(sender, label); return CommandResult.SUCCESS; } - private static void downloadTranslations(LuckPermsPlugin plugin, List languages, Sender sender) { - try { - MoreFiles.createDirectoriesIfNotExists(plugin.getTranslationManager().getTranslationsDirectory()); - } catch (IOException e) { - // ignore - } - - for (LanguageInfo language : languages) { - Message.TRANSLATIONS_INSTALLING_SPECIFIC.send(sender, language.locale.toString()); - - Request request = new Request.Builder() - .header("User-Agent", plugin.getBytebin().getUserAgent()) - .url(TRANSLATIONS_DOWNLOAD_ENDPOINT + language.id) - .build(); - - Path file = plugin.getTranslationManager().getTranslationsDirectory().resolve(language.locale.toString() + ".properties"); - - try (Response response = plugin.getBytebin().makeHttpRequest(request)) { - try (ResponseBody responseBody = response.body()) { - if (responseBody == null) { - throw new RuntimeException("No response"); - } - - try (InputStream inputStream = responseBody.byteStream()) { - Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING); - } - } - } catch (UnsuccessfulRequestException | IOException e) { - Message.TRANSLATIONS_DOWNLOAD_ERROR.send(sender, language.locale.toString()); - plugin.getLogger().warn("Unable to download translations", e); - } - } - } - - public static List getAvailableTranslations(LuckPermsPlugin plugin) throws IOException, UnsuccessfulRequestException { - Request request = new Request.Builder() - .header("User-Agent", plugin.getBytebin().getUserAgent()) - .url(TRANSLATIONS_INFO_ENDPOINT) - .build(); - - JsonObject jsonResponse; - try (Response response = plugin.getBytebin().makeHttpRequest(request)) { - try (ResponseBody responseBody = response.body()) { - if (responseBody == null) { - throw new RuntimeException("No response"); - } - - try (InputStream inputStream = responseBody.byteStream()) { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - jsonResponse = GsonProvider.normal().fromJson(reader, JsonObject.class); - } - } - } - } - - List languages = new ArrayList<>(); - for (Map.Entry language : jsonResponse.get("languages").getAsJsonObject().entrySet()) { - languages.add(new LanguageInfo(language.getKey(), language.getValue().getAsJsonObject())); - } - languages.removeIf(language -> language.progress <= 0); - return languages; - } - - private static String localeDisplayName(Locale locale) { - if (locale.getLanguage().equals("zh")) { - if (locale.getCountry().equals("CN")) { - return "简体中文"; // Chinese (Simplified) - } else if (locale.getCountry().equals("TW")) { - return "繁體中文"; // Chinese (Traditional) - } - return locale.getDisplayCountry(locale) + locale.getDisplayLanguage(locale); - } - - if (locale.getLanguage().equals("en") && locale.getCountry().equals("PT")) { - return "Pirate"; - } - - return locale.getDisplayLanguage(locale); - } - - private static final class LanguageInfo { - private final String id; - private final String name; - private final Locale locale; - private final int progress; - private final List contributors; - - LanguageInfo(String id, JsonObject data) { - this.id = id; - this.name = data.get("name").getAsString(); - this.locale = Objects.requireNonNull(TranslationManager.parseLocale(data.get("localeTag").getAsString())); - this.progress = data.get("progress").getAsInt(); - this.contributors = new ArrayList<>(); - for (JsonElement contributor : data.get("contributors").getAsJsonArray()) { - this.contributors.add(contributor.getAsJsonObject().get("name").getAsString()); - } - } - } } diff --git a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java index e6625cfc9..742c0a2b5 100644 --- a/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java +++ b/common/src/main/java/me/lucko/luckperms/common/config/ConfigKeys.java @@ -416,6 +416,11 @@ public final class ConfigKeys { .collect(ImmutableCollectors.toList()); }); + /** + * If LuckPerms should automatically install translation bundles and periodically update them. + */ + public static final ConfigKey AUTO_INSTALL_TRANSLATIONS = notReloadable(booleanKey("auto-install-translations", true)); + /** * If auto op is enabled. Only used by the Bukkit platform. */ diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java index 96b3f4721..f14f83efb 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationManager.java @@ -159,7 +159,6 @@ public class TranslationManager { } this.registry.registerAll(locale, bundle, false); - this.plugin.getLogger().info("Registered additional translations for " + locale.toString()); this.installed.add(locale); return Maps.immutableEntry(locale, bundle); } @@ -186,4 +185,21 @@ public class TranslationManager { return locale == null ? null : Translator.parseLocale(locale); } + public static String localeDisplayName(Locale locale) { + if (locale.getLanguage().equals("zh")) { + if (locale.getCountry().equals("CN")) { + return "简体中文"; // Chinese (Simplified) + } else if (locale.getCountry().equals("TW")) { + return "繁體中文"; // Chinese (Traditional) + } + return locale.getDisplayCountry(locale) + locale.getDisplayLanguage(locale); + } + + if (locale.getLanguage().equals("en") && locale.getCountry().equals("PT")) { + return "Pirate"; + } + + return locale.getDisplayLanguage(locale); + } + } diff --git a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java new file mode 100644 index 000000000..b6eb18dbb --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java @@ -0,0 +1,329 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * 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. + */ + +package me.lucko.luckperms.common.locale; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import me.lucko.luckperms.common.config.ConfigKeys; +import me.lucko.luckperms.common.http.UnsuccessfulRequestException; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.sender.Sender; +import me.lucko.luckperms.common.util.MoreFiles; +import me.lucko.luckperms.common.util.gson.GsonProvider; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class TranslationRepository { + private static final String TRANSLATIONS_INFO_ENDPOINT = "https://metadata.luckperms.net/data/translations"; + private static final String TRANSLATIONS_DOWNLOAD_ENDPOINT = "https://metadata.luckperms.net/translation/"; + private static final long MAX_BUNDLE_SIZE = 1048576L; // 1mb + private static final long CACHE_MAX_AGE = TimeUnit.DAYS.toMillis(1); + + private final LuckPermsPlugin plugin; + + public TranslationRepository(LuckPermsPlugin plugin) { + this.plugin = plugin; + } + + /** + * Gets a list of available languages. + * + * @return a list of languages + * @throws IOException if an i/o error occurs + * @throws UnsuccessfulRequestException if the http request fails + */ + public List getAvailableLanguages() throws IOException, UnsuccessfulRequestException { + return getTranslationsMetadata().languages; + } + + /** + * Schedules a refresh of the current translations if necessary. + */ + public void scheduleRefresh() { + if (!this.plugin.getConfiguration().get(ConfigKeys.AUTO_INSTALL_TRANSLATIONS)) { + return; // skip + } + + this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + try { + refresh(); + } catch (Exception e) { + // ignore + } + }); + } + + private void refresh() throws Exception { + Path translationsDirectory = this.plugin.getTranslationManager().getTranslationsDirectory(); + try { + MoreFiles.createDirectoriesIfNotExists(translationsDirectory); + } catch (IOException e) { + // ignore + } + + long lastRefresh = 0L; + + Path repoStatusFile = translationsDirectory.resolve("repository.json"); + if (Files.exists(repoStatusFile)) { + try (BufferedReader reader = Files.newBufferedReader(repoStatusFile, StandardCharsets.UTF_8)) { + JsonObject status = GsonProvider.normal().fromJson(reader, JsonObject.class); + if (status.has("lastRefresh")) { + lastRefresh = status.get("lastRefresh").getAsLong(); + } + } catch (Exception e) { + // ignore + } + } + + long timeSinceLastRefresh = System.currentTimeMillis() - lastRefresh; + if (timeSinceLastRefresh <= CACHE_MAX_AGE) { + return; + } + + MetadataResponse metadata = getTranslationsMetadata(); + + if (timeSinceLastRefresh <= metadata.cacheMaxAge) { + return; + } + + // perform a refresh! + downloadAndInstallTranslations(metadata.languages, null, true); + } + + /** + * Downloads and installs translations for the given languages. + * + * @param languages the languages to install translations for + * @param sender the sender to report progress to + * @param updateStatus if the status file should be updated + */ + public void downloadAndInstallTranslations(List languages, @Nullable Sender sender, boolean updateStatus) { + TranslationManager manager = this.plugin.getTranslationManager(); + Path translationsDirectory = manager.getTranslationsDirectory(); + + try { + MoreFiles.createDirectoriesIfNotExists(translationsDirectory); + } catch (IOException e) { + // ignore + } + + for (LanguageInfo language : languages) { + if (sender != null) { + Message.TRANSLATIONS_INSTALLING_SPECIFIC.send(sender, language.locale().toString()); + } + + Request request = new Request.Builder() + .header("User-Agent", this.plugin.getBytebin().getUserAgent()) + .url(TRANSLATIONS_DOWNLOAD_ENDPOINT + language.id()) + .build(); + + Path file = translationsDirectory.resolve(language.locale().toString() + ".properties"); + + try (Response response = this.plugin.getBytebin().makeHttpRequest(request)) { + try (ResponseBody responseBody = response.body()) { + if (responseBody == null) { + throw new IOException("No response"); + } + + try (InputStream inputStream = new LimitedInputStream(responseBody.byteStream(), MAX_BUNDLE_SIZE)) { + Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING); + } + } + } catch (UnsuccessfulRequestException | IOException e) { + if (sender != null) { + Message.TRANSLATIONS_DOWNLOAD_ERROR.send(sender, language.locale().toString()); + this.plugin.getLogger().warn("Unable to download translations", e); + } + } + } + + if (updateStatus) { + // update status file + Path repoStatusFile = translationsDirectory.resolve("repository.json"); + try (BufferedWriter writer = Files.newBufferedWriter(repoStatusFile, StandardCharsets.UTF_8)) { + JsonObject status = new JsonObject(); + status.add("lastRefresh", new JsonPrimitive(System.currentTimeMillis())); + GsonProvider.prettyPrinting().toJson(status, writer); + } catch (IOException e) { + // ignore + } + } + + this.plugin.getTranslationManager().reload(); + } + + private MetadataResponse getTranslationsMetadata() throws IOException, UnsuccessfulRequestException { + Request request = new Request.Builder() + .header("User-Agent", this.plugin.getBytebin().getUserAgent()) + .url(TRANSLATIONS_INFO_ENDPOINT) + .build(); + + JsonObject jsonResponse; + try (Response response = this.plugin.getBytebin().makeHttpRequest(request)) { + try (ResponseBody responseBody = response.body()) { + if (responseBody == null) { + throw new RuntimeException("No response"); + } + + try (InputStream inputStream = new LimitedInputStream(responseBody.byteStream(), MAX_BUNDLE_SIZE)) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + jsonResponse = GsonProvider.normal().fromJson(reader, JsonObject.class); + } + } + } + } + + List languages = new ArrayList<>(); + for (Map.Entry language : jsonResponse.get("languages").getAsJsonObject().entrySet()) { + languages.add(new LanguageInfo(language.getKey(), language.getValue().getAsJsonObject())); + } + languages.removeIf(language -> language.progress() <= 0); + + if (languages.size() >= 100) { + // just a precaution: if more than 100 languages have been + // returned then the metadata server is doing something silly + throw new IOException("More than 100 languages - cancelling download"); + } + + long cacheMaxAge = jsonResponse.get("cacheMaxAge").getAsLong(); + + return new MetadataResponse(cacheMaxAge, languages); + } + + private static final class MetadataResponse { + private final long cacheMaxAge; + private final List languages; + + MetadataResponse(long cacheMaxAge, List languages) { + this.cacheMaxAge = cacheMaxAge; + this.languages = languages; + } + } + + public static final class LanguageInfo { + private final String id; + private final String name; + private final Locale locale; + private final int progress; + private final List contributors; + + LanguageInfo(String id, JsonObject data) { + this.id = id; + this.name = data.get("name").getAsString(); + this.locale = Objects.requireNonNull(TranslationManager.parseLocale(data.get("localeTag").getAsString())); + this.progress = data.get("progress").getAsInt(); + this.contributors = new ArrayList<>(); + for (JsonElement contributor : data.get("contributors").getAsJsonArray()) { + this.contributors.add(contributor.getAsJsonObject().get("name").getAsString()); + } + } + + public String id() { + return this.id; + } + + public String name() { + return this.name; + } + + public Locale locale() { + return this.locale; + } + + public int progress() { + return this.progress; + } + + public List contributors() { + return this.contributors; + } + } + + private static final class LimitedInputStream extends FilterInputStream implements Closeable { + private final long limit; + private long count; + + public LimitedInputStream(InputStream inputStream, long limit) { + super(inputStream); + this.limit = limit; + } + + private void checkLimit() throws IOException { + if (this.count > this.limit) { + throw new IOException("Limit exceeded"); + } + } + + @Override + public int read() throws IOException { + int res = super.read(); + if (res != -1) { + this.count++; + checkLimit(); + } + return res; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int res = super.read(b, off, len); + if (res > 0) { + this.count += res; + checkLimit(); + } + return res; + } + + @Override + public void close() throws IOException { + super.close(); + } + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java index 236382881..8cb03ab1c 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/AbstractLuckPermsPlugin.java @@ -43,6 +43,7 @@ import me.lucko.luckperms.common.http.BytebinClient; import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; import me.lucko.luckperms.common.locale.Message; import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.locale.TranslationRepository; import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.messaging.MessagingFactory; import me.lucko.luckperms.common.plugin.logging.PluginLogger; @@ -79,6 +80,7 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { private LogDispatcher logDispatcher; private LuckPermsConfiguration configuration; private BytebinClient bytebin; + private TranslationRepository translationRepository; private FileWatcher fileWatcher = null; private Storage storage; private InternalMessagingService messagingService = null; @@ -124,6 +126,10 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { this.bytebin = new BytebinClient(httpClient, getConfiguration().get(ConfigKeys.BYTEBIN_URL), "luckperms"); + // init translation repo and update bundle files + this.translationRepository = new TranslationRepository(this); + this.translationRepository.scheduleRefresh(); + // now the configuration is loaded, we can create a storage factory and load initial dependencies StorageFactory storageFactory = new StorageFactory(this); Set storageTypes = storageFactory.getRequiredTypes(); @@ -320,6 +326,11 @@ public abstract class AbstractLuckPermsPlugin implements LuckPermsPlugin { return this.bytebin; } + @Override + public TranslationRepository getTranslationRepository() { + return this.translationRepository; + } + @Override public Optional getFileWatcher() { return Optional.ofNullable(this.fileWatcher); diff --git a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java index ffbd27351..718261baa 100644 --- a/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java +++ b/common/src/main/java/me/lucko/luckperms/common/plugin/LuckPermsPlugin.java @@ -38,6 +38,7 @@ import me.lucko.luckperms.common.extension.SimpleExtensionManager; import me.lucko.luckperms.common.http.BytebinClient; import me.lucko.luckperms.common.inheritance.InheritanceGraphFactory; import me.lucko.luckperms.common.locale.TranslationManager; +import me.lucko.luckperms.common.locale.TranslationRepository; import me.lucko.luckperms.common.messaging.InternalMessagingService; import me.lucko.luckperms.common.model.Group; import me.lucko.luckperms.common.model.Track; @@ -175,6 +176,13 @@ public interface LuckPermsPlugin { */ TranslationManager getTranslationManager(); + /** + * Gets the translation repository + * + * @return the translation repository + */ + TranslationRepository getTranslationRepository(); + /** * Gets the dependency manager for the plugin * diff --git a/nukkit/src/main/resources/config.yml b/nukkit/src/main/resources/config.yml index 7dbe900df..3809238fb 100644 --- a/nukkit/src/main/resources/config.yml +++ b/nukkit/src/main/resources/config.yml @@ -305,6 +305,9 @@ log-notify: true log-notify-filtered-descriptions: # - "parent add example" +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + # Defines the options for prefix and suffix stacking. # # - The feature allows you to display multiple prefixes or suffixes alongside a players username in diff --git a/sponge/src/main/resources/luckperms.conf b/sponge/src/main/resources/luckperms.conf index 73a9cefc6..a1a315c8c 100644 --- a/sponge/src/main/resources/luckperms.conf +++ b/sponge/src/main/resources/luckperms.conf @@ -315,6 +315,9 @@ log-notify-filtered-descriptions = [ # "parent add example" ] +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations = true + # Defines the options for prefix and suffix stacking. # # - The feature allows you to display multiple prefixes or suffixes alongside a players username in diff --git a/velocity/src/main/resources/config.yml b/velocity/src/main/resources/config.yml index 2ac7baad1..106307982 100644 --- a/velocity/src/main/resources/config.yml +++ b/velocity/src/main/resources/config.yml @@ -309,6 +309,9 @@ log-notify: true log-notify-filtered-descriptions: # - "parent add example" +# If LuckPerms should automatically install translation bundles and periodically update them. +auto-install-translations: true + # Defines the options for prefix and suffix stacking. # # - The feature allows you to display multiple prefixes or suffixes alongside a players username in