From b2c76aca7da8751a2927bac534d76253fbed0e1c Mon Sep 17 00:00:00 2001 From: Luck Date: Sun, 10 Oct 2021 13:26:33 +0100 Subject: [PATCH] Improve translations handling (#3166) --- .../common/locale/TranslationManager.java | 66 +++++++++++--- .../common/locale/TranslationRepository.java | 91 +++++++++++-------- 2 files changed, 105 insertions(+), 52 deletions(-) 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 d02094a57..f574107ac 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 @@ -28,6 +28,7 @@ package me.lucko.luckperms.common.locale; import com.google.common.collect.Maps; import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.util.MoreFiles; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; @@ -60,19 +61,39 @@ public class TranslationManager { public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; private final LuckPermsPlugin plugin; - private final Path translationsDirectory; private final Set installed = ConcurrentHashMap.newKeySet(); private TranslationRegistry registry; + private final Path translationsDirectory; + private final Path repositoryTranslationsDirectory; + private final Path customTranslationsDirectory; + public TranslationManager(LuckPermsPlugin plugin) { this.plugin = plugin; this.translationsDirectory = this.plugin.getBootstrap().getConfigDirectory().resolve("translations"); + this.repositoryTranslationsDirectory = this.translationsDirectory.resolve("repository"); + this.customTranslationsDirectory = this.translationsDirectory.resolve("custom"); + + try { + MoreFiles.createDirectoriesIfNotExists(this.repositoryTranslationsDirectory); + MoreFiles.createDirectoriesIfNotExists(this.customTranslationsDirectory); + } catch (IOException e) { + // ignore + } } public Path getTranslationsDirectory() { return this.translationsDirectory; } + public Path getRepositoryTranslationsDirectory() { + return this.repositoryTranslationsDirectory; + } + + public Path getRepositoryStatusFile() { + return this.repositoryTranslationsDirectory.resolve("status.json"); + } + public Set getInstalledLocales() { return Collections.unmodifiableSet(this.installed); } @@ -89,8 +110,9 @@ public class TranslationManager { this.registry.defaultLocale(DEFAULT_LOCALE); // load custom translations first, then the base (built-in) translations after. - loadCustom(); - loadBase(); + loadFromFileSystem(this.customTranslationsDirectory, false); + loadFromFileSystem(this.repositoryTranslationsDirectory, true); + loadFromResourceBundle(); // register it to the global source, so our translations can be picked up by adventure-platform GlobalTranslator.get().addSource(this.registry); @@ -99,36 +121,45 @@ public class TranslationManager { /** * Loads the base (English) translations from the jar file. */ - private void loadBase() { + private void loadFromResourceBundle() { ResourceBundle bundle = ResourceBundle.getBundle("luckperms", DEFAULT_LOCALE, UTF8ResourceBundleControl.get()); try { this.registry.registerAll(DEFAULT_LOCALE, bundle, false); } catch (IllegalArgumentException e) { - this.plugin.getLogger().warn("Error loading default locale file", e); + if (!isAdventureDuplicatesException(e)) { + this.plugin.getLogger().warn("Error loading default locale file", e); + } } } + public static boolean isTranslationFile(Path path) { + return path.getFileName().toString().endsWith(".properties"); + } + /** * Loads custom translations (in any language) from the plugin configuration folder. */ - public void loadCustom() { + public void loadFromFileSystem(Path directory, boolean suppressDuplicatesError) { List translationFiles; - try (Stream stream = Files.list(this.translationsDirectory)) { - translationFiles = stream.filter(path -> path.getFileName().toString().endsWith(".properties")).collect(Collectors.toList()); + try (Stream stream = Files.list(directory)) { + translationFiles = stream.filter(TranslationManager::isTranslationFile).collect(Collectors.toList()); } catch (IOException e) { translationFiles = Collections.emptyList(); } + if (translationFiles.isEmpty()) { + return; + } + Map loaded = new HashMap<>(); for (Path translationFile : translationFiles) { try { - Map.Entry result = loadCustomTranslationFile(translationFile); + Map.Entry result = loadTranslationFile(translationFile); loaded.put(result.getKey(), result.getValue()); - } catch (IllegalArgumentException e) { - // common error is from adventure "java.lang.IllegalArgumentException: Invalid key" -- don't print the whole stack trace. - this.plugin.getLogger().warn("Error loading locale file: " + translationFile.getFileName() + " - " + e); } catch (Exception e) { - this.plugin.getLogger().warn("Error loading locale file: " + translationFile.getFileName(), e); + if (!suppressDuplicatesError || !isAdventureDuplicatesException(e)) { + this.plugin.getLogger().warn("Error loading locale file: " + translationFile.getFileName(), e); + } } } @@ -139,13 +170,13 @@ public class TranslationManager { try { this.registry.registerAll(localeWithoutCountry, bundle, false); } catch (IllegalArgumentException e) { - // ignore "IllegalArgumentException: Invalid key" from adventure TranslationRegistry + // ignore } } }); } - private Map.Entry loadCustomTranslationFile(Path translationFile) throws IOException { + private Map.Entry loadTranslationFile(Path translationFile) throws IOException { String fileName = translationFile.getFileName().toString(); String localeString = fileName.substring(0, fileName.length() - ".properties".length()); Locale locale = parseLocale(localeString); @@ -164,6 +195,11 @@ public class TranslationManager { return Maps.immutableEntry(locale, bundle); } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private static boolean isAdventureDuplicatesException(Exception e) { + return e instanceof IllegalArgumentException && (e.getMessage().startsWith("Invalid key") || e.getMessage().startsWith("Translation already exists")); + } + public static Component render(Component component) { return render(component, Locale.getDefault()); } 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 index b71a98bc7..f787c6ca9 100644 --- a/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java +++ b/common/src/main/java/me/lucko/luckperms/common/locale/TranslationRepository.java @@ -33,7 +33,6 @@ 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; @@ -59,6 +58,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; public class TranslationRepository { private static final String TRANSLATIONS_INFO_ENDPOINT = "https://metadata.luckperms.net/data/translations"; @@ -92,6 +92,9 @@ public class TranslationRepository { } this.plugin.getBootstrap().getScheduler().executeAsync(() -> { + // cleanup old translation files + clearDirectory(this.plugin.getTranslationManager().getTranslationsDirectory(), Files::isRegularFile); + try { refresh(); } catch (Exception e) { @@ -101,34 +104,14 @@ public class TranslationRepository { } 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 lastRefresh = readLastRefreshTime(); long timeSinceLastRefresh = System.currentTimeMillis() - lastRefresh; + if (timeSinceLastRefresh <= CACHE_MAX_AGE) { return; } MetadataResponse metadata = getTranslationsMetadata(); - if (timeSinceLastRefresh <= metadata.cacheMaxAge) { return; } @@ -137,6 +120,22 @@ public class TranslationRepository { downloadAndInstallTranslations(metadata.languages, null, true); } + private void clearDirectory(Path directory, Predicate predicate) { + try { + Files.list(directory) + .filter(predicate) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + // ignore + } + }); + } catch (IOException e) { + // ignore + } + } + /** * Downloads and installs translations for the given languages. * @@ -146,13 +145,10 @@ public class TranslationRepository { */ public void downloadAndInstallTranslations(List languages, @Nullable Sender sender, boolean updateStatus) { TranslationManager manager = this.plugin.getTranslationManager(); - Path translationsDirectory = manager.getTranslationsDirectory(); + Path translationsDirectory = manager.getRepositoryTranslationsDirectory(); - try { - MoreFiles.createDirectoriesIfNotExists(translationsDirectory); - } catch (IOException e) { - // ignore - } + // clear existing translations + clearDirectory(translationsDirectory, TranslationManager::isTranslationFile); for (LanguageInfo language : languages) { if (sender != null) { @@ -185,18 +181,39 @@ public class TranslationRepository { } 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) { + writeLastRefreshTime(); + } + + manager.reload(); + } + + private void writeLastRefreshTime() { + Path statusFile = this.plugin.getTranslationManager().getRepositoryStatusFile(); + + try (BufferedWriter writer = Files.newBufferedWriter(statusFile, StandardCharsets.UTF_8)) { + JsonObject status = new JsonObject(); + status.add("lastRefresh", new JsonPrimitive(System.currentTimeMillis())); + GsonProvider.prettyPrinting().toJson(status, writer); + } catch (IOException e) { + // ignore + } + } + + private long readLastRefreshTime() { + Path statusFile = this.plugin.getTranslationManager().getRepositoryStatusFile(); + + if (Files.exists(statusFile)) { + try (BufferedReader reader = Files.newBufferedReader(statusFile, StandardCharsets.UTF_8)) { + JsonObject status = GsonProvider.normal().fromJson(reader, JsonObject.class); + if (status.has("lastRefresh")) { + return status.get("lastRefresh").getAsLong(); + } + } catch (Exception e) { // ignore } } - this.plugin.getTranslationManager().reload(); + return 0L; } private MetadataResponse getTranslationsMetadata() throws IOException, UnsuccessfulRequestException {