1
0
mirror of https://github.com/lucko/LuckPerms.git synced 2025-09-01 18:32:33 +02:00

Automatically install translation bundles

This commit is contained in:
Luck
2020-11-12 00:28:02 +00:00
parent 62270bfd93
commit 129a10aa60
11 changed files with 389 additions and 126 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<LanguageInfo> 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<LanguageInfo> 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<LanguageInfo> 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<LanguageInfo> languages = new ArrayList<>();
for (Map.Entry<String, JsonElement> 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<String> 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());
}
}
}
}

View File

@@ -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<Boolean> AUTO_INSTALL_TRANSLATIONS = notReloadable(booleanKey("auto-install-translations", true));
/**
* If auto op is enabled. Only used by the Bukkit platform.
*/

View File

@@ -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);
}
}

View File

@@ -0,0 +1,329 @@
/*
* This file is part of LuckPerms, licensed under the MIT License.
*
* Copyright (c) lucko (Luck) <luck@lucko.me>
* 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<LanguageInfo> 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<LanguageInfo> 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<LanguageInfo> languages = new ArrayList<>();
for (Map.Entry<String, JsonElement> 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<LanguageInfo> languages;
MetadataResponse(long cacheMaxAge, List<LanguageInfo> 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<String> 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<String> 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();
}
}
}

View File

@@ -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<StorageType> 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<FileWatcher> getFileWatcher() {
return Optional.ofNullable(this.fileWatcher);

View File

@@ -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
*

View File

@@ -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

View File

@@ -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

View File

@@ -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