diff --git a/plugin/src/main/java/com/lishid/openinv/OpenInv.java b/plugin/src/main/java/com/lishid/openinv/OpenInv.java
index cfca819..25b28aa 100644
--- a/plugin/src/main/java/com/lishid/openinv/OpenInv.java
+++ b/plugin/src/main/java/com/lishid/openinv/OpenInv.java
@@ -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()) {
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java
index 406b472..c8dd3c7 100644
--- a/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java
+++ b/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java
@@ -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;
}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java
index 8117697..d4f193a 100644
--- a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java
+++ b/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java
@@ -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;
}
}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
index 37e7c53..bef78bb 100644
--- a/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
+++ b/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
@@ -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;
}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
index c79aa43..2c52f5d 100644
--- a/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
+++ b/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
@@ -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;
}
diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
index 3b2cbfb..60235bd 100644
--- a/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
+++ b/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
@@ -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;
}
diff --git a/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java b/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java
index a7f4a95..ea7c438 100644
--- a/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java
+++ b/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java
@@ -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);
}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java b/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java
deleted file mode 100644
index ff526b1..0000000
--- a/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java
+++ /dev/null
@@ -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 .
- */
-
-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 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 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 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;
- }
-
-}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/lang/LanguageManager.java b/plugin/src/main/java/com/lishid/openinv/util/lang/LanguageManager.java
new file mode 100644
index 0000000..239031e
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/lang/LanguageManager.java
@@ -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 .
+ */
+
+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 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 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 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 getMissingKeys(
+ @NotNull Configuration configurationDefault,
+ @NotNull Predicate nodeSetPredicate) {
+ List 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;
+ }
+
+}
diff --git a/plugin/src/main/java/com/lishid/openinv/util/lang/Replacement.java b/plugin/src/main/java/com/lishid/openinv/util/lang/Replacement.java
new file mode 100644
index 0000000..6753044
--- /dev/null
+++ b/plugin/src/main/java/com/lishid/openinv/util/lang/Replacement.java
@@ -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 .
+ */
+
+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) {
+
+}