Improve translations handling (#3166)

This commit is contained in:
Luck 2021-10-10 13:26:33 +01:00
parent 04bb035a83
commit b2c76aca7d
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
2 changed files with 105 additions and 52 deletions

View File

@ -28,6 +28,7 @@ package me.lucko.luckperms.common.locale;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.util.MoreFiles;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -60,19 +61,39 @@ public class TranslationManager {
public static final Locale DEFAULT_LOCALE = Locale.ENGLISH; public static final Locale DEFAULT_LOCALE = Locale.ENGLISH;
private final LuckPermsPlugin plugin; private final LuckPermsPlugin plugin;
private final Path translationsDirectory;
private final Set<Locale> installed = ConcurrentHashMap.newKeySet(); private final Set<Locale> installed = ConcurrentHashMap.newKeySet();
private TranslationRegistry registry; private TranslationRegistry registry;
private final Path translationsDirectory;
private final Path repositoryTranslationsDirectory;
private final Path customTranslationsDirectory;
public TranslationManager(LuckPermsPlugin plugin) { public TranslationManager(LuckPermsPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
this.translationsDirectory = this.plugin.getBootstrap().getConfigDirectory().resolve("translations"); 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() { public Path getTranslationsDirectory() {
return this.translationsDirectory; return this.translationsDirectory;
} }
public Path getRepositoryTranslationsDirectory() {
return this.repositoryTranslationsDirectory;
}
public Path getRepositoryStatusFile() {
return this.repositoryTranslationsDirectory.resolve("status.json");
}
public Set<Locale> getInstalledLocales() { public Set<Locale> getInstalledLocales() {
return Collections.unmodifiableSet(this.installed); return Collections.unmodifiableSet(this.installed);
} }
@ -89,8 +110,9 @@ public class TranslationManager {
this.registry.defaultLocale(DEFAULT_LOCALE); this.registry.defaultLocale(DEFAULT_LOCALE);
// load custom translations first, then the base (built-in) translations after. // load custom translations first, then the base (built-in) translations after.
loadCustom(); loadFromFileSystem(this.customTranslationsDirectory, false);
loadBase(); loadFromFileSystem(this.repositoryTranslationsDirectory, true);
loadFromResourceBundle();
// register it to the global source, so our translations can be picked up by adventure-platform // register it to the global source, so our translations can be picked up by adventure-platform
GlobalTranslator.get().addSource(this.registry); GlobalTranslator.get().addSource(this.registry);
@ -99,36 +121,45 @@ public class TranslationManager {
/** /**
* Loads the base (English) translations from the jar file. * Loads the base (English) translations from the jar file.
*/ */
private void loadBase() { private void loadFromResourceBundle() {
ResourceBundle bundle = ResourceBundle.getBundle("luckperms", DEFAULT_LOCALE, UTF8ResourceBundleControl.get()); ResourceBundle bundle = ResourceBundle.getBundle("luckperms", DEFAULT_LOCALE, UTF8ResourceBundleControl.get());
try { try {
this.registry.registerAll(DEFAULT_LOCALE, bundle, false); this.registry.registerAll(DEFAULT_LOCALE, bundle, false);
} catch (IllegalArgumentException e) { } 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. * Loads custom translations (in any language) from the plugin configuration folder.
*/ */
public void loadCustom() { public void loadFromFileSystem(Path directory, boolean suppressDuplicatesError) {
List<Path> translationFiles; List<Path> translationFiles;
try (Stream<Path> stream = Files.list(this.translationsDirectory)) { try (Stream<Path> stream = Files.list(directory)) {
translationFiles = stream.filter(path -> path.getFileName().toString().endsWith(".properties")).collect(Collectors.toList()); translationFiles = stream.filter(TranslationManager::isTranslationFile).collect(Collectors.toList());
} catch (IOException e) { } catch (IOException e) {
translationFiles = Collections.emptyList(); translationFiles = Collections.emptyList();
} }
if (translationFiles.isEmpty()) {
return;
}
Map<Locale, ResourceBundle> loaded = new HashMap<>(); Map<Locale, ResourceBundle> loaded = new HashMap<>();
for (Path translationFile : translationFiles) { for (Path translationFile : translationFiles) {
try { try {
Map.Entry<Locale, ResourceBundle> result = loadCustomTranslationFile(translationFile); Map.Entry<Locale, ResourceBundle> result = loadTranslationFile(translationFile);
loaded.put(result.getKey(), result.getValue()); 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) { } 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 { try {
this.registry.registerAll(localeWithoutCountry, bundle, false); this.registry.registerAll(localeWithoutCountry, bundle, false);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// ignore "IllegalArgumentException: Invalid key" from adventure TranslationRegistry // ignore
} }
} }
}); });
} }
private Map.Entry<Locale, ResourceBundle> loadCustomTranslationFile(Path translationFile) throws IOException { private Map.Entry<Locale, ResourceBundle> loadTranslationFile(Path translationFile) throws IOException {
String fileName = translationFile.getFileName().toString(); String fileName = translationFile.getFileName().toString();
String localeString = fileName.substring(0, fileName.length() - ".properties".length()); String localeString = fileName.substring(0, fileName.length() - ".properties".length());
Locale locale = parseLocale(localeString); Locale locale = parseLocale(localeString);
@ -164,6 +195,11 @@ public class TranslationManager {
return Maps.immutableEntry(locale, bundle); 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) { public static Component render(Component component) {
return render(component, Locale.getDefault()); return render(component, Locale.getDefault());
} }

View File

@ -33,7 +33,6 @@ import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.http.UnsuccessfulRequestException; import me.lucko.luckperms.common.http.UnsuccessfulRequestException;
import me.lucko.luckperms.common.plugin.LuckPermsPlugin; import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import me.lucko.luckperms.common.sender.Sender; import me.lucko.luckperms.common.sender.Sender;
import me.lucko.luckperms.common.util.MoreFiles;
import me.lucko.luckperms.common.util.gson.GsonProvider; import me.lucko.luckperms.common.util.gson.GsonProvider;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@ -59,6 +58,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class TranslationRepository { public class TranslationRepository {
private static final String TRANSLATIONS_INFO_ENDPOINT = "https://metadata.luckperms.net/data/translations"; 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(() -> { this.plugin.getBootstrap().getScheduler().executeAsync(() -> {
// cleanup old translation files
clearDirectory(this.plugin.getTranslationManager().getTranslationsDirectory(), Files::isRegularFile);
try { try {
refresh(); refresh();
} catch (Exception e) { } catch (Exception e) {
@ -101,34 +104,14 @@ public class TranslationRepository {
} }
private void refresh() throws Exception { private void refresh() throws Exception {
Path translationsDirectory = this.plugin.getTranslationManager().getTranslationsDirectory(); long lastRefresh = readLastRefreshTime();
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; long timeSinceLastRefresh = System.currentTimeMillis() - lastRefresh;
if (timeSinceLastRefresh <= CACHE_MAX_AGE) { if (timeSinceLastRefresh <= CACHE_MAX_AGE) {
return; return;
} }
MetadataResponse metadata = getTranslationsMetadata(); MetadataResponse metadata = getTranslationsMetadata();
if (timeSinceLastRefresh <= metadata.cacheMaxAge) { if (timeSinceLastRefresh <= metadata.cacheMaxAge) {
return; return;
} }
@ -137,6 +120,22 @@ public class TranslationRepository {
downloadAndInstallTranslations(metadata.languages, null, true); downloadAndInstallTranslations(metadata.languages, null, true);
} }
private void clearDirectory(Path directory, Predicate<Path> 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. * Downloads and installs translations for the given languages.
* *
@ -146,13 +145,10 @@ public class TranslationRepository {
*/ */
public void downloadAndInstallTranslations(List<LanguageInfo> languages, @Nullable Sender sender, boolean updateStatus) { public void downloadAndInstallTranslations(List<LanguageInfo> languages, @Nullable Sender sender, boolean updateStatus) {
TranslationManager manager = this.plugin.getTranslationManager(); TranslationManager manager = this.plugin.getTranslationManager();
Path translationsDirectory = manager.getTranslationsDirectory(); Path translationsDirectory = manager.getRepositoryTranslationsDirectory();
try { // clear existing translations
MoreFiles.createDirectoriesIfNotExists(translationsDirectory); clearDirectory(translationsDirectory, TranslationManager::isTranslationFile);
} catch (IOException e) {
// ignore
}
for (LanguageInfo language : languages) { for (LanguageInfo language : languages) {
if (sender != null) { if (sender != null) {
@ -185,18 +181,39 @@ public class TranslationRepository {
} }
if (updateStatus) { if (updateStatus) {
// update status file writeLastRefreshTime();
Path repoStatusFile = translationsDirectory.resolve("repository.json"); }
try (BufferedWriter writer = Files.newBufferedWriter(repoStatusFile, StandardCharsets.UTF_8)) {
JsonObject status = new JsonObject(); manager.reload();
status.add("lastRefresh", new JsonPrimitive(System.currentTimeMillis())); }
GsonProvider.prettyPrinting().toJson(status, writer);
} catch (IOException e) { 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 // ignore
} }
} }
this.plugin.getTranslationManager().reload(); return 0L;
} }
private MetadataResponse getTranslationsMetadata() throws IOException, UnsuccessfulRequestException { private MetadataResponse getTranslationsMetadata() throws IOException, UnsuccessfulRequestException {