Improve language manager (#116)
* Clarify replacements internally to reduce the likelihood of messing up when including new ones * Fix missing keys not being written to disk for easy translation * Use a guessfile-like system where missing translation keys are inserted into a separate section for easy identification * Reduce verbosity of missing keys warning
This commit is contained in:
		@@ -28,9 +28,10 @@ import com.lishid.openinv.internal.ISpecialEnderChest;
 | 
			
		||||
import com.lishid.openinv.internal.ISpecialInventory;
 | 
			
		||||
import com.lishid.openinv.internal.ISpecialPlayerInventory;
 | 
			
		||||
import com.lishid.openinv.util.ConfigUpdater;
 | 
			
		||||
import com.lishid.openinv.util.LanguageManager;
 | 
			
		||||
import com.lishid.openinv.util.Permissions;
 | 
			
		||||
import com.lishid.openinv.util.StringMetric;
 | 
			
		||||
import com.lishid.openinv.util.lang.LanguageManager;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
@@ -463,7 +464,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
 | 
			
		||||
    public @Nullable String getLocalizedMessage(
 | 
			
		||||
            @NotNull CommandSender sender,
 | 
			
		||||
            @NotNull String key,
 | 
			
		||||
            String @NotNull ... replacements) {
 | 
			
		||||
            Replacement @NotNull ... replacements) {
 | 
			
		||||
        return this.languageManager.getValue(key, getLocale(sender), replacements);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -483,7 +484,7 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void sendMessage(@NotNull CommandSender sender, @NotNull String key, String @NotNull... replacements) {
 | 
			
		||||
    public void sendMessage(@NotNull CommandSender sender, @NotNull String key, Replacement @NotNull... replacements) {
 | 
			
		||||
        String message = getLocalizedMessage(sender, key, replacements);
 | 
			
		||||
 | 
			
		||||
        if (message != null && !message.isEmpty()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ package com.lishid.openinv.commands;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import com.lishid.openinv.util.TabCompleter;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
@@ -69,7 +70,11 @@ public class ContainerSettingCommand implements TabExecutor {
 | 
			
		||||
            onOff = String.valueOf(getSetting.test(player));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        plugin.sendMessage(sender, "messages.info.settingState","%setting%", any ? "AnyContainer" : "SilentContainer", "%state%", onOff);
 | 
			
		||||
        plugin.sendMessage(
 | 
			
		||||
                sender,
 | 
			
		||||
                "messages.info.settingState",
 | 
			
		||||
                new Replacement("%setting%", any ? "AnyContainer" : "SilentContainer"),
 | 
			
		||||
                new Replacement("%state%", onOff));
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import com.lishid.openinv.OpenInv;
 | 
			
		||||
import com.lishid.openinv.internal.ISpecialInventory;
 | 
			
		||||
import com.lishid.openinv.util.Permissions;
 | 
			
		||||
import com.lishid.openinv.util.TabCompleter;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
@@ -175,16 +176,20 @@ public class OpenInvCommand implements TabExecutor {
 | 
			
		||||
            // Protected check
 | 
			
		||||
            if (!Permissions.OVERRIDE.hasPermission(player)
 | 
			
		||||
                    && Permissions.EXEMPT.hasPermission(onlineTarget)) {
 | 
			
		||||
                plugin.sendMessage(player, "messages.error.permissionExempt",
 | 
			
		||||
                        "%target%", onlineTarget.getDisplayName());
 | 
			
		||||
                plugin.sendMessage(
 | 
			
		||||
                        player,
 | 
			
		||||
                        "messages.error.permissionExempt",
 | 
			
		||||
                        new Replacement("%target%", onlineTarget.getDisplayName()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Crossworld check
 | 
			
		||||
            if (!Permissions.CROSSWORLD.hasPermission(player)
 | 
			
		||||
                    && !onlineTarget.getWorld().equals(player.getWorld())) {
 | 
			
		||||
                plugin.sendMessage(player, "messages.error.permissionCrossWorld",
 | 
			
		||||
                        "%target%", onlineTarget.getDisplayName());
 | 
			
		||||
                plugin.sendMessage(
 | 
			
		||||
                        player,
 | 
			
		||||
                        "messages.error.permissionCrossWorld",
 | 
			
		||||
                        new Replacement("%target%", onlineTarget.getDisplayName()));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ package com.lishid.openinv.commands;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import com.lishid.openinv.util.TabCompleter;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import org.bukkit.Chunk;
 | 
			
		||||
@@ -57,7 +58,10 @@ public class SearchContainerCommand implements TabExecutor {
 | 
			
		||||
        Material material = Material.getMaterial(args[0].toUpperCase());
 | 
			
		||||
 | 
			
		||||
        if (material == null) {
 | 
			
		||||
            plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args[0]);
 | 
			
		||||
            plugin.sendMessage(
 | 
			
		||||
                    sender,
 | 
			
		||||
                    "messages.error.invalidMaterial",
 | 
			
		||||
                    new Replacement("%target%", args[0]));
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -100,13 +104,18 @@ public class SearchContainerCommand implements TabExecutor {
 | 
			
		||||
        if (locations.length() > 0) {
 | 
			
		||||
            locations.delete(locations.length() - 2, locations.length());
 | 
			
		||||
        } else {
 | 
			
		||||
            plugin.sendMessage(sender, "messages.info.container.noMatches",
 | 
			
		||||
                    "%target%", material.name());
 | 
			
		||||
            plugin.sendMessage(
 | 
			
		||||
                    sender,
 | 
			
		||||
                    "messages.info.container.noMatches",
 | 
			
		||||
                    new Replacement("%target%", material.name()));
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        plugin.sendMessage(sender, "messages.info.container.matches",
 | 
			
		||||
                "%target%", material.name(), "%detail%", locations.toString());
 | 
			
		||||
        plugin.sendMessage(
 | 
			
		||||
                sender,
 | 
			
		||||
                "messages.info.container.matches",
 | 
			
		||||
                new Replacement("%target%", material.name()),
 | 
			
		||||
                new Replacement("%detail%", locations.toString()));
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ package com.lishid.openinv.commands;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import com.lishid.openinv.util.TabCompleter;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import org.bukkit.Material;
 | 
			
		||||
@@ -114,14 +115,18 @@ public class SearchEnchantCommand implements TabExecutor {
 | 
			
		||||
            // Matches found, delete trailing comma and space
 | 
			
		||||
            players.delete(players.length() - 2, players.length());
 | 
			
		||||
        } else {
 | 
			
		||||
            plugin.sendMessage(sender, "messages.info.player.noMatches",
 | 
			
		||||
                    "%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level);
 | 
			
		||||
            plugin.sendMessage(
 | 
			
		||||
                    sender,
 | 
			
		||||
                    "messages.info.player.noMatches",
 | 
			
		||||
                    new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level));
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        plugin.sendMessage(sender, "messages.info.player.matches",
 | 
			
		||||
                "%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level,
 | 
			
		||||
                "%detail%", players.toString());
 | 
			
		||||
        plugin.sendMessage(
 | 
			
		||||
                sender,
 | 
			
		||||
                "messages.info.player.matches",
 | 
			
		||||
                new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level),
 | 
			
		||||
                        new Replacement("%detail%", players.toString()));
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2011-2021 lishid. All rights reserved.
 | 
			
		||||
 * Copyright (C) 2011-2022 lishid. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
@@ -18,6 +18,7 @@ package com.lishid.openinv.commands;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import com.lishid.openinv.util.TabCompleter;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import org.bukkit.Material;
 | 
			
		||||
@@ -46,7 +47,10 @@ public class SearchInvCommand implements TabExecutor {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (material == null) {
 | 
			
		||||
            plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args.length > 0 ? args[0] : "null");
 | 
			
		||||
            plugin.sendMessage(
 | 
			
		||||
                    sender,
 | 
			
		||||
                    "messages.error.invalidMaterial",
 | 
			
		||||
                    new Replacement("%target%", args.length > 0 ? args[0] : "null"));
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -56,7 +60,10 @@ public class SearchInvCommand implements TabExecutor {
 | 
			
		||||
            try {
 | 
			
		||||
                count = Integer.parseInt(args[1]);
 | 
			
		||||
            } catch (NumberFormatException ex) {
 | 
			
		||||
                plugin.sendMessage(sender, "messages.error.invalidNumber", "%target%", args[1]);
 | 
			
		||||
                plugin.sendMessage(
 | 
			
		||||
                        sender,
 | 
			
		||||
                        "messages.error.invalidNumber",
 | 
			
		||||
                        new Replacement("%target%", args[1]));
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -74,13 +81,18 @@ public class SearchInvCommand implements TabExecutor {
 | 
			
		||||
        if (players.length() > 0) {
 | 
			
		||||
            players.delete(players.length() - 2, players.length());
 | 
			
		||||
        } else {
 | 
			
		||||
            plugin.sendMessage(sender, "messages.info.player.noMatches",
 | 
			
		||||
                    "%target%", material.name());
 | 
			
		||||
            plugin.sendMessage(
 | 
			
		||||
                    sender,
 | 
			
		||||
                    "messages.info.player.noMatches",
 | 
			
		||||
                    new Replacement("%target%", material.name()));
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        plugin.sendMessage(sender, "messages.info.player.matches",
 | 
			
		||||
                "%target%", material.name(), "%detail%", players.toString());
 | 
			
		||||
        plugin.sendMessage(
 | 
			
		||||
                sender,
 | 
			
		||||
                "messages.info.player.matches",
 | 
			
		||||
                new Replacement("%target%", material.name()),
 | 
			
		||||
                new Replacement("%detail%", players.toString()));
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
package com.lishid.openinv.internal;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import com.lishid.openinv.util.lang.Replacement;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import org.bukkit.entity.HumanEntity;
 | 
			
		||||
import org.bukkit.entity.Player;
 | 
			
		||||
@@ -73,8 +74,7 @@ public class OpenInventoryView extends InventoryView {
 | 
			
		||||
                    .getLocalizedMessage(
 | 
			
		||||
                            player,
 | 
			
		||||
                            titleKey,
 | 
			
		||||
                            "%player%",
 | 
			
		||||
                            owner.getName());
 | 
			
		||||
                            new Replacement("%player%", owner.getName()));
 | 
			
		||||
            title = Objects.requireNonNullElseGet(localTitle, () -> owner.getName() + titleDefaultSuffix);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,168 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2011-2022 lishid. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, version 3.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package com.lishid.openinv.util;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import java.io.BufferedReader;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.InputStreamReader;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import org.bukkit.ChatColor;
 | 
			
		||||
import org.bukkit.configuration.file.YamlConfiguration;
 | 
			
		||||
import org.jetbrains.annotations.NotNull;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple language manager supporting both custom and bundled languages.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Jikoo
 | 
			
		||||
 */
 | 
			
		||||
public class LanguageManager {
 | 
			
		||||
 | 
			
		||||
    private final OpenInv plugin;
 | 
			
		||||
    private final String defaultLocale;
 | 
			
		||||
    private final Map<String, YamlConfiguration> locales;
 | 
			
		||||
 | 
			
		||||
    public LanguageManager(@NotNull OpenInv plugin, @NotNull String defaultLocale) {
 | 
			
		||||
        this.plugin = plugin;
 | 
			
		||||
        this.defaultLocale = defaultLocale;
 | 
			
		||||
        this.locales = new HashMap<>();
 | 
			
		||||
        getOrLoadLocale(defaultLocale);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private @NotNull YamlConfiguration getOrLoadLocale(@NotNull String locale) {
 | 
			
		||||
        YamlConfiguration loaded = locales.get(locale);
 | 
			
		||||
        if (loaded != null) {
 | 
			
		||||
            return loaded;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        InputStream resourceStream = plugin.getResource("locale/" + locale + ".yml");
 | 
			
		||||
        YamlConfiguration localeConfigDefaults;
 | 
			
		||||
        if (resourceStream == null) {
 | 
			
		||||
            localeConfigDefaults = new YamlConfiguration();
 | 
			
		||||
        } else {
 | 
			
		||||
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) {
 | 
			
		||||
                localeConfigDefaults = YamlConfiguration.loadConfiguration(reader);
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to load resource " + locale + ".yml", e);
 | 
			
		||||
                localeConfigDefaults = new YamlConfiguration();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        File file = new File(plugin.getDataFolder(), locale + ".yml");
 | 
			
		||||
        YamlConfiguration localeConfig;
 | 
			
		||||
 | 
			
		||||
        if (!file.exists()) {
 | 
			
		||||
            localeConfig = localeConfigDefaults;
 | 
			
		||||
            try {
 | 
			
		||||
                localeConfigDefaults.save(file);
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to save resource " + locale + ".yml", e);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            localeConfig = YamlConfiguration.loadConfiguration(file);
 | 
			
		||||
 | 
			
		||||
            // Add new language keys
 | 
			
		||||
            List<String> newKeys = new ArrayList<>();
 | 
			
		||||
            for (String key : localeConfigDefaults.getKeys(true)) {
 | 
			
		||||
                if (localeConfigDefaults.isConfigurationSection(key)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (localeConfig.isSet(key)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                localeConfig.set(key, localeConfigDefaults.get(key));
 | 
			
		||||
                newKeys.add(key);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!newKeys.isEmpty()) {
 | 
			
		||||
                plugin.getLogger().info("[LanguageManager] Added new language keys: " + String.join(", ", newKeys));
 | 
			
		||||
                try {
 | 
			
		||||
                    localeConfig.save(file);
 | 
			
		||||
                } catch (IOException e) {
 | 
			
		||||
                    plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to save resource " + locale + ".yml", e);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!locale.equals(defaultLocale)) {
 | 
			
		||||
            localeConfigDefaults = locales.get(defaultLocale);
 | 
			
		||||
 | 
			
		||||
            // Check for missing keys
 | 
			
		||||
            List<String> newKeys = new ArrayList<>();
 | 
			
		||||
            for (String key : localeConfigDefaults.getKeys(true)) {
 | 
			
		||||
                if (localeConfigDefaults.isConfigurationSection(key)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (localeConfig.isSet(key)) {
 | 
			
		||||
                    continue;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                newKeys.add(key);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!newKeys.isEmpty()) {
 | 
			
		||||
                plugin.getLogger().info("[LanguageManager] Missing translations from " + locale + ".yml: " + String.join(", ", newKeys));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Fall through to default locale
 | 
			
		||||
            localeConfig.setDefaults(localeConfigDefaults);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        locales.put(locale, localeConfig);
 | 
			
		||||
        return localeConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public @Nullable String getValue(@NotNull String key, @Nullable String locale) {
 | 
			
		||||
        String value = getOrLoadLocale(locale == null ? defaultLocale : locale.toLowerCase()).getString(key);
 | 
			
		||||
        if (value == null || value.isEmpty()) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        value = ChatColor.translateAlternateColorCodes('&', value);
 | 
			
		||||
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public @Nullable String getValue(@NotNull String key, @Nullable String locale, String @NotNull ... replacements) {
 | 
			
		||||
        if (replacements.length % 2 != 0) {
 | 
			
		||||
            plugin.getLogger().log(Level.WARNING, "[LanguageManager] Replacement data is uneven", new Exception());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String value = getValue(key, locale);
 | 
			
		||||
 | 
			
		||||
        if (value == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (int i = 0; i < replacements.length; i += 2) {
 | 
			
		||||
            value = value.replace(replacements[i], replacements[i + 1]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,217 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2011-2022 lishid. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, version 3.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package com.lishid.openinv.util.lang;
 | 
			
		||||
 | 
			
		||||
import com.lishid.openinv.OpenInv;
 | 
			
		||||
import java.io.BufferedReader;
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.io.InputStream;
 | 
			
		||||
import java.io.InputStreamReader;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.function.Predicate;
 | 
			
		||||
import java.util.logging.Level;
 | 
			
		||||
import org.bukkit.ChatColor;
 | 
			
		||||
import org.bukkit.configuration.Configuration;
 | 
			
		||||
import org.bukkit.configuration.ConfigurationSection;
 | 
			
		||||
import org.bukkit.configuration.file.YamlConfiguration;
 | 
			
		||||
import org.jetbrains.annotations.NotNull;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A simple language manager supporting both custom and bundled languages.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Jikoo
 | 
			
		||||
 */
 | 
			
		||||
public class LanguageManager {
 | 
			
		||||
 | 
			
		||||
    private final OpenInv plugin;
 | 
			
		||||
    private final String defaultLocale;
 | 
			
		||||
    private final Map<String, YamlConfiguration> locales;
 | 
			
		||||
 | 
			
		||||
    public LanguageManager(@NotNull OpenInv plugin, @NotNull String defaultLocale) {
 | 
			
		||||
        this.plugin = plugin;
 | 
			
		||||
        this.defaultLocale = defaultLocale;
 | 
			
		||||
        this.locales = new HashMap<>();
 | 
			
		||||
        getOrLoadLocale(defaultLocale);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private @NotNull YamlConfiguration getOrLoadLocale(@NotNull String locale) {
 | 
			
		||||
        YamlConfiguration loaded = locales.get(locale);
 | 
			
		||||
        if (loaded != null) {
 | 
			
		||||
            return loaded;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        File file = new File(plugin.getDataFolder(), locale + ".yml");
 | 
			
		||||
 | 
			
		||||
        // Load locale config from disk and bundled locale defaults.
 | 
			
		||||
        YamlConfiguration localeConfig = loadLocale(locale, file);
 | 
			
		||||
 | 
			
		||||
        // If the locale is not the default locale, also handle any missing translations from the default locale.
 | 
			
		||||
        if (!locale.equals(defaultLocale)) {
 | 
			
		||||
            addTranslationFallthrough(locale, localeConfig, file);
 | 
			
		||||
 | 
			
		||||
            if (plugin.getConfig().getBoolean("settings.secret.warn-about-guess-section", true)
 | 
			
		||||
                    && localeConfig.isConfigurationSection("guess")) {
 | 
			
		||||
                // Warn that guess section exists. This should run once per language per server restart
 | 
			
		||||
                // when accessed by a user to hint to server owners that they can make UX improvements.
 | 
			
		||||
                plugin.getLogger().info(() -> "[LanguageManager] Missing translations from " + locale + ".yml! Check the guess section!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        locales.put(locale, localeConfig);
 | 
			
		||||
        return localeConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private @NotNull YamlConfiguration loadLocale(
 | 
			
		||||
            @NotNull String locale,
 | 
			
		||||
            @NotNull File file) {
 | 
			
		||||
        // Load defaults from the plugin's bundled resources.
 | 
			
		||||
        InputStream resourceStream = plugin.getResource("locale/" + locale + ".yml");
 | 
			
		||||
        YamlConfiguration localeConfigDefaults;
 | 
			
		||||
        if (resourceStream == null) {
 | 
			
		||||
            localeConfigDefaults = new YamlConfiguration();
 | 
			
		||||
        } else {
 | 
			
		||||
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) {
 | 
			
		||||
                localeConfigDefaults = YamlConfiguration.loadConfiguration(reader);
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to load resource " + locale + ".yml");
 | 
			
		||||
                localeConfigDefaults = new YamlConfiguration();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!file.exists()) {
 | 
			
		||||
            // If the file does not exist on disk, save bundled defaults.
 | 
			
		||||
            try {
 | 
			
		||||
                localeConfigDefaults.save(file);
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + locale + ".yml");
 | 
			
		||||
            }
 | 
			
		||||
            // Return loaded bundled locale.
 | 
			
		||||
            return localeConfigDefaults;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the file does exist on disk, load it.
 | 
			
		||||
        YamlConfiguration localeConfig = YamlConfiguration.loadConfiguration(file);
 | 
			
		||||
        // Check for missing translations from the bundled file.
 | 
			
		||||
        List<String> newKeys = getMissingKeys(localeConfigDefaults, localeConfig::isSet);
 | 
			
		||||
 | 
			
		||||
        if (newKeys.isEmpty()) {
 | 
			
		||||
            return localeConfig;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Get guess section for missing keys.
 | 
			
		||||
        ConfigurationSection guess = localeConfig.getConfigurationSection("guess");
 | 
			
		||||
 | 
			
		||||
        for (String newKey : newKeys) {
 | 
			
		||||
            // Set all missing keys to defaults.
 | 
			
		||||
            localeConfig.set(newKey, localeConfigDefaults.get(newKey));
 | 
			
		||||
 | 
			
		||||
            // Delete relevant guess keys in case this is a new translation.
 | 
			
		||||
            if (guess != null) {
 | 
			
		||||
                guess.set(newKey, null);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If guess section is empty, delete it.
 | 
			
		||||
        if (guess != null && guess.getKeys(false).isEmpty()) {
 | 
			
		||||
            localeConfig.set("guess", null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        plugin.getLogger().info(() -> "[LanguageManager] Added new translation keys to " + locale + ".yml: " + String.join(", ", newKeys));
 | 
			
		||||
 | 
			
		||||
        // Write new keys to disk.
 | 
			
		||||
        try {
 | 
			
		||||
            localeConfig.save(file);
 | 
			
		||||
        } catch (IOException e) {
 | 
			
		||||
            plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + locale + ".yml");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return localeConfig;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void addTranslationFallthrough(
 | 
			
		||||
            @NotNull String locale,
 | 
			
		||||
            @NotNull YamlConfiguration localeConfig,
 | 
			
		||||
            @NotNull File file) {
 | 
			
		||||
        YamlConfiguration defaultLocaleConfig = locales.get(defaultLocale);
 | 
			
		||||
 | 
			
		||||
        // Get missing keys. Keys that already have a guess value are not new and don't need to trigger another write.
 | 
			
		||||
        List<String> missingKeys = getMissingKeys(
 | 
			
		||||
                defaultLocaleConfig,
 | 
			
		||||
                key -> localeConfig.isSet(key) || localeConfig.isSet("guess." + key));
 | 
			
		||||
 | 
			
		||||
        if (!missingKeys.isEmpty()) {
 | 
			
		||||
            // Set up guess section for missing keys.
 | 
			
		||||
            for (String key : missingKeys) {
 | 
			
		||||
                localeConfig.set("guess." + key, defaultLocaleConfig.get(key));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Write modified guess section to disk.
 | 
			
		||||
            try {
 | 
			
		||||
                localeConfig.save(file);
 | 
			
		||||
            } catch (IOException e) {
 | 
			
		||||
                plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + locale + ".yml");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Fall through to default locale.
 | 
			
		||||
        localeConfig.setDefaults(defaultLocaleConfig);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private @NotNull List<String> getMissingKeys(
 | 
			
		||||
            @NotNull Configuration configurationDefault,
 | 
			
		||||
            @NotNull Predicate<String> nodeSetPredicate) {
 | 
			
		||||
        List<String> missingKeys = new ArrayList<>();
 | 
			
		||||
        for (String key : configurationDefault.getKeys(true)) {
 | 
			
		||||
            if (!configurationDefault.isConfigurationSection(key) && !nodeSetPredicate.test(key)) {
 | 
			
		||||
                // Missing keys are non-section keys that fail the predicate.
 | 
			
		||||
                missingKeys.add(key);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return missingKeys;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public @Nullable String getValue(@NotNull String key, @Nullable String locale) {
 | 
			
		||||
        String value = getOrLoadLocale(locale == null ? defaultLocale : locale.toLowerCase()).getString(key);
 | 
			
		||||
        if (value == null || value.isEmpty()) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        value = ChatColor.translateAlternateColorCodes('&', value);
 | 
			
		||||
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public @Nullable String getValue(@NotNull String key, @Nullable String locale, Replacement @NotNull ... replacements) {
 | 
			
		||||
        String value = getValue(key, locale);
 | 
			
		||||
 | 
			
		||||
        if (value == null) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (Replacement replacement : replacements) {
 | 
			
		||||
            value = value.replace(replacement.placeholder(), replacement.value());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return value;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2011-2022 lishid. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, version 3.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package com.lishid.openinv.util.lang;
 | 
			
		||||
 | 
			
		||||
import org.jetbrains.annotations.NotNull;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A data holder for string replacement in translations.
 | 
			
		||||
 *
 | 
			
		||||
 * @param placeholder the placeholder to be replaced
 | 
			
		||||
 * @param value the value to insert
 | 
			
		||||
 */
 | 
			
		||||
public record Replacement(@NotNull String placeholder, @NotNull String value) {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user