diff --git a/src/main/java/dev/loki/loparkour/config/LocaleCache.java b/src/main/java/dev/loki/loparkour/config/LocaleCache.java new file mode 100644 index 0000000..3168958 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/config/LocaleCache.java @@ -0,0 +1,70 @@ +package dev.loki.loparkour.config; + +import org.bukkit.configuration.file.FileConfiguration; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.Function; + +/** + * Thread-safe cache for loaded locale configurations. + */ +public class LocaleCache { + + private final Map locales = new HashMap<>(); + + /** + * Replace all cached locales with a new map. + */ + public synchronized void setLocales(@NotNull Map newLocales) { + locales.clear(); + locales.putAll(newLocales); + } + + /** + * Get a value from a locale configuration, with fallback. + */ + @NotNull + public synchronized T cachedValue(@NotNull String locale, + @NotNull Function extractor, + @NotNull T defaultValue) { + if (locales.isEmpty()) return defaultValue; + FileConfiguration config = locales.get(locale); + if (config == null) return defaultValue; + try { + T result = extractor.apply(config); + return result != null ? result : defaultValue; + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get a locale configuration by key. + */ + public synchronized FileConfiguration getLocale(@NotNull String locale) { + return locales.get(locale); + } + + /** + * Check if locales are loaded. + */ + public synchronized boolean isEmpty() { + return locales.isEmpty(); + } + + /** + * Get the number of loaded locales. + */ + public synchronized int getLocaleCount() { + return locales.size(); + } + + /** + * Get all locale keys. + */ + @NotNull + public synchronized Set getLocaleKeys() { + return new HashSet<>(locales.keySet()); + } +} diff --git a/src/main/java/dev/loki/loparkour/config/LocaleLoader.java b/src/main/java/dev/loki/loparkour/config/LocaleLoader.java new file mode 100644 index 0000000..8b1d1f4 --- /dev/null +++ b/src/main/java/dev/loki/loparkour/config/LocaleLoader.java @@ -0,0 +1,107 @@ +package dev.loki.loparkour.config; + +import dev.loki.loparkour.LoParkour; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.stream.Stream; + +/** + * Loads locale files from disk with validation and auto-fix. + */ +public class LocaleLoader { + + /** + * Load all locale files from the locales/ folder. + * Returns a map of locale name → FileConfiguration. + */ + @NotNull + public static Map loadAll() { + Plugin plugin = LoParkour.getPlugin(); + try { + FileConfiguration embedded = YamlConfiguration.loadConfiguration( + new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8)); + + return loadFromDisk(embedded); + } catch (Exception ex) { + plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage()); + return Collections.emptyMap(); + } + } + + @NotNull + private static Map loadFromDisk(@NotNull FileConfiguration embedded) throws Exception { + Map result = new HashMap<>(); + File folder = LoParkour.getInFolder("locales"); + ensureFolder(folder); + + try (Stream stream = Files.list(folder.toPath())) { + stream.forEach(path -> { + File file = path.toFile(); + if (!file.getName().endsWith(".yml")) return; + + String locale = file.getName().split("\\.")[0]; + LoParkour.log("Found locale " + locale); + + FileConfiguration config = YamlConfiguration.loadConfiguration(file); + validateAndFix(embedded, config, file); + result.put(locale, config); + }); + } + return result; + } + + private static void ensureFolder(@NotNull File folder) { + if (!folder.exists()) folder.mkdirs(); + String[] files = folder.list(); + if (files == null || files.length == 0) { + Plugin plugin = LoParkour.getPlugin(); + plugin.saveResource("locales/en.yml", false); + plugin.saveResource("locales/ru.yml", false); + } + } + + /** + * Validate a locale file against the reference (en.yml) and auto-fix missing keys. + */ + private static void validateAndFix(@NotNull FileConfiguration reference, + @NotNull FileConfiguration locale, + @NotNull File file) { + List refNodes = getChildren(reference); + List locNodes = getChildren(locale); + + boolean modified = false; + for (String node : refNodes) { + if (!locNodes.contains(node)) { + LoParkour.log("Fixing missing config node %s in %s".formatted(node, file.getName())); + locale.set(node, reference.get(node)); + modified = true; + } + } + if (modified) { + try { + locale.save(file); + } catch (IOException ex) { + LoParkour.getPlugin().getLogger().severe( + "Error saving fixed locale %s — delete and restart — %s".formatted(file.getName(), ex.getMessage())); + } + } + LoParkour.log("Validated locale " + file.getName()); + } + + @NotNull + private static List getChildren(@NotNull FileConfiguration config) { + ConfigurationSection section = config.getConfigurationSection(""); + return section != null ? new ArrayList<>(section.getKeys(true)) : Collections.emptyList(); + } +} diff --git a/src/main/java/dev/loki/loparkour/config/Locales.java b/src/main/java/dev/loki/loparkour/config/Locales.java index 570ef0f..e4e08b2 100644 --- a/src/main/java/dev/loki/loparkour/config/Locales.java +++ b/src/main/java/dev/loki/loparkour/config/Locales.java @@ -5,49 +5,42 @@ import dev.loki.loparkour.menu.ParkourOption; import dev.loki.loparkour.player.ParkourUser; import dev.loki.loparkour.util.Item; import dev.loki.loparkour.util.Materials; -import dev.lolib.scheduler.Scheduler; import org.bukkit.Material; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.*; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Stream; /** - * Locale system: loading, caching, validation, and access. + * Facade for locale system: loading, caching, and access. + * Delegates to LocaleLoader and LocaleCache. */ public class Locales { - private static final Map locales = new HashMap<>(); + private static final LocaleCache cache = new LocaleCache(); private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("%[a-z]"); - // ── public API ──────────────────────────────────────────────────────────── + // ── Initialization ──────────────────────────────────────────────────────── public static void init() { - loadLocalesAsync(); + Map loaded = LocaleLoader.loadAll(); + cache.setLocales(loaded); + LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)"); } + // ── Public API ──────────────────────────────────────────────────────────── + @NotNull public static String getString(@NotNull Player player, @NotNull String path) { return getString(getPlayerLocale(player), path); } /** - * Player: that player's locale. Console / command blocks: default language from config ({@link Option#OPTIONS_DEFAULTS}), or {@code en}. + * Player: that player's locale. Console / command blocks: default language from config, or {@code en}. */ @NotNull public static String getString(@NotNull CommandSender sender, @NotNull String path) { @@ -65,21 +58,21 @@ public class Locales { @NotNull public static String getString(@NotNull String locale, @NotNull String path) { - return cachedValue(locale, c -> c.getString(path), ""); + return cache.cachedValue(locale, c -> c.getString(path), ""); } @NotNull public static List getStringList(@NotNull String locale, @NotNull String path) { - return cachedValue(locale, c -> c.getStringList(path), Collections.emptyList()); + return cache.cachedValue(locale, c -> c.getStringList(path), Collections.emptyList()); } public static int getLocaleCount() { - synchronized (locales) { return locales.size(); } + return cache.getLocaleCount(); } @NotNull public static Set getLocaleKeys() { - synchronized (locales) { return new HashSet<>(locales.keySet()); } + return cache.getLocaleKeys(); } @NotNull @@ -89,118 +82,20 @@ public class Locales { @NotNull public static Item getItem(@NotNull String locale, @NotNull String path, String... replace) { - synchronized (locales) { - if (locales.isEmpty()) { - LoParkour.getPlugin().getLogger().warning("Locales are empty! Returning STONE item for path: " + path); - return new Item(Material.STONE, ""); - } - FileConfiguration config = locales.get(locale); - if (config == null) { - LoParkour.getPlugin().getLogger().warning("Locale '" + locale + "' not found! Available: " + locales.keySet() + ". Returning STONE for path: " + path); - return new Item(Material.STONE, ""); - } - return buildItem(config, path, replace); + if (cache.isEmpty()) { + LoParkour.getPlugin().getLogger().warning("Locales are empty! Returning STONE item for path: " + path); + return new Item(Material.STONE, ""); } + FileConfiguration config = cache.getLocale(locale); + if (config == null) { + LoParkour.getPlugin().getLogger().warning( + "Locale '" + locale + "' not found! Available: " + cache.getLocaleKeys() + ". Returning STONE for path: " + path); + return new Item(Material.STONE, ""); + } + return buildItem(config, path, replace); } - // ── loading (was LocaleLoader) ──────────────────────────────────────────── - - private static void loadLocalesAsync() { - Plugin plugin = LoParkour.getPlugin(); - try { - FileConfiguration embedded = YamlConfiguration.loadConfiguration( - new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8)); - - Map loaded = loadFromDisk(embedded); - - synchronized (locales) { - locales.clear(); - locales.putAll(loaded); - } - LoParkour.log("Locales loaded successfully (" + loaded.size() + " locales)"); - } catch (Exception ex) { - plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage()); - } - } - - @NotNull - private static Map loadFromDisk(@NotNull FileConfiguration embedded) throws Exception { - Map result = new HashMap<>(); - File folder = LoParkour.getInFolder("locales"); - ensureFolder(folder); - - try (Stream stream = Files.list(folder.toPath())) { - stream.forEach(path -> { - File file = path.toFile(); - if (!file.getName().endsWith(".yml")) return; - - String locale = file.getName().split("\\.")[0]; - LoParkour.log("Found locale " + locale); - - FileConfiguration config = YamlConfiguration.loadConfiguration(file); - validateAndFix(embedded, config, file); - result.put(locale, config); - }); - } - return result; - } - - private static void ensureFolder(@NotNull File folder) { - if (!folder.exists()) folder.mkdirs(); - String[] files = folder.list(); - if (files == null || files.length == 0) { - Plugin plugin = LoParkour.getPlugin(); - plugin.saveResource("locales/en.yml", false); - plugin.saveResource("locales/ru.yml", false); - } - } - - // ── validation (was LocaleValidator) ────────────────────────────────────── - - private static void validateAndFix(@NotNull FileConfiguration reference, @NotNull FileConfiguration locale, @NotNull File file) { - List refNodes = getChildren(reference); - List locNodes = getChildren(locale); - - boolean modified = false; - for (String node : refNodes) { - if (!locNodes.contains(node)) { - LoParkour.log("Fixing missing config node %s in %s".formatted(node, file.getName())); - locale.set(node, reference.get(node)); - modified = true; - } - } - if (modified) { - try { locale.save(file); } - catch (IOException ex) { - LoParkour.getPlugin().getLogger().severe( - "Error saving fixed locale %s — delete and restart — %s".formatted(file.getName(), ex.getMessage())); - } - } - LoParkour.log("Validated locale " + file.getName()); - } - - @NotNull - private static List getChildren(@NotNull FileConfiguration config) { - ConfigurationSection section = config.getConfigurationSection(""); - return section != null ? new ArrayList<>(section.getKeys(true)) : Collections.emptyList(); - } - - // ── cache access (was LocaleCache) ──────────────────────────────────────── - - @NotNull - private static T cachedValue(@NotNull String locale, @NotNull Function extractor, @NotNull T defaultValue) { - synchronized (locales) { - if (locales.isEmpty()) return defaultValue; - FileConfiguration config = locales.get(locale); - if (config == null) return defaultValue; - try { - T result = extractor.apply(config); - return result != null ? result : defaultValue; - } catch (Exception e) { return defaultValue; } - } - } - - // ── item builder ────────────────────────────────────────────────────────── + // ── Item builder ────────────────────────────────────────────────────────── @NotNull private static Item buildItem(@NotNull FileConfiguration config, @NotNull String path, String... replace) { @@ -221,7 +116,8 @@ public class Locales { Material mat = Materials.parse(material); if (mat == null) { - LoParkour.getPlugin().getLogger().warning("Invalid material '%s' for locale path '%s', using STONE".formatted(material, path)); + LoParkour.getPlugin().getLogger().warning( + "Invalid material '%s' for locale path '%s', using STONE".formatted(material, path)); mat = Material.STONE; }