From 68786310c34bd685a3e1fb808240d8ea0de1c42a Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 10 Jul 2026 08:54:23 -0700 Subject: [PATCH] Add optional manual material/entity name translations in locale files Material and entity names in the limits GUI and hit-limit messages were always the prettified English Bukkit IDs. Locale files may now provide translations under island.limits.materials. and island.limits.entities. (lowercase Bukkit names), following the same pattern as Challenges. Anything not listed falls back to the automatic English name, so the sections are entirely optional. Fixes #188 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015dbJyrv2uxHfTmiWkqGUJB --- .../world/bentobox/limits/DisplayNames.java | 44 ++++++ .../limits/commands/player/LimitTab.java | 11 +- .../limits/listeners/BlockLimitsListener.java | 3 +- .../limits/listeners/EntityLimitListener.java | 28 ++-- src/main/resources/locales/en-US.yml | 145 +++++++++--------- .../bentobox/limits/DisplayNamesTest.java | 50 ++++++ 6 files changed, 194 insertions(+), 87 deletions(-) create mode 100644 src/main/java/world/bentobox/limits/DisplayNames.java create mode 100644 src/test/java/world/bentobox/limits/DisplayNamesTest.java diff --git a/src/main/java/world/bentobox/limits/DisplayNames.java b/src/main/java/world/bentobox/limits/DisplayNames.java new file mode 100644 index 0000000..e7d698b --- /dev/null +++ b/src/main/java/world/bentobox/limits/DisplayNames.java @@ -0,0 +1,44 @@ +package world.bentobox.limits; + +import java.util.Locale; + +import org.bukkit.NamespacedKey; +import org.bukkit.entity.EntityType; + +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; + +/** + * Resolves the display name of a limited material or entity for a user. + * + *

Locale files may provide manual translations under + * {@code island.limits.materials.} and {@code island.limits.entities.} + * (lowercase Bukkit names). Any key without a translation falls back to the automatic + * prettified English name, so translations are entirely optional. + */ +public final class DisplayNames { + + private DisplayNames() { + } + + /** + * @param user the user whose locale is used + * @param key the material key, e.g. {@code minecraft:hopper} + * @return translated material name, or the prettified key if no translation exists + */ + public static String material(User user, NamespacedKey key) { + String translated = user.getTranslationOrNothing("island.limits.materials." + key.getKey()); + return translated == null || translated.isEmpty() ? Util.prettifyText(key.getKey()) : translated; + } + + /** + * @param user the user whose locale is used + * @param type the entity type + * @return translated entity name, or the prettified type name if no translation exists + */ + public static String entity(User user, EntityType type) { + String translated = user + .getTranslationOrNothing("island.limits.entities." + type.toString().toLowerCase(Locale.ROOT)); + return translated == null || translated.isEmpty() ? Util.prettifyText(type.toString()) : translated; + } +} diff --git a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java index e5acaa8..94d29eb 100644 --- a/src/main/java/world/bentobox/limits/commands/player/LimitTab.java +++ b/src/main/java/world/bentobox/limits/commands/player/LimitTab.java @@ -26,6 +26,7 @@ import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; import world.bentobox.bentobox.api.user.User; import world.bentobox.bentobox.util.Util; +import world.bentobox.limits.DisplayNames; import world.bentobox.limits.EntityGroup; import world.bentobox.limits.Limits; import world.bentobox.limits.objects.IslandBlockCount; @@ -112,7 +113,7 @@ private void addEntityGroupLimits(IslandBlockCount ibc) { PanelItemBuilder pib = new PanelItemBuilder(); pib.name(user.getTranslation("island.limits.panel.entity-group-name-syntax", TextVariables.NAME, g.getName())); - String description = "(" + prettyNames(g) + ")\n"; + String description = "(" + prettyNames(user, g) + ")\n"; pib.icon(g.getIcon()); int count = ibc == null ? 0 : sumGroupCount(ibc, g); String color = count >= limit ? user.getTranslation(MAX_COLOR_KEY) @@ -144,7 +145,7 @@ private void addEntityLimits(IslandBlockCount ibc) { map.forEach((k, v) -> { PanelItemBuilder pib = new PanelItemBuilder(); pib.name(user.getTranslation("island.limits.panel.entity-name-syntax", TextVariables.NAME, - Util.prettifyText(k.toString()))); + DisplayNames.entity(user, k))); Material m; try { if (E2M.containsKey(k)) { @@ -172,7 +173,7 @@ private void addMaterialIcons(IslandBlockCount ibc, Map for (Entry en : matLimits.entrySet()) { PanelItemBuilder pib = new PanelItemBuilder(); pib.name(user.getTranslation("island.limits.panel.block-name-syntax", TextVariables.NAME, - Util.prettifyText(en.getKey().getKey()))); + DisplayNames.material(user, en.getKey()))); Material mat = Registry.MATERIAL.get(B2M.getOrDefault(en.getKey(), en.getKey())); pib.icon(Objects.requireNonNullElse(mat, Material.PAPER)); @@ -222,11 +223,11 @@ public String getPermission() { return ""; } - private String prettyNames(EntityGroup v) { + private String prettyNames(User user, EntityGroup v) { StringBuilder sb = new StringBuilder(); List l = new ArrayList<>(v.getTypes()); for (int i = 0; i < l.size(); i++) { - sb.append(Util.prettifyText(l.get(i).toString())); + sb.append(DisplayNames.entity(user, l.get(i))); if (i + 1 < l.size()) sb.append(", "); if ((i + 1) % 5 == 0) sb.append("\n"); } diff --git a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java index 41c2ebb..4206bb4 100644 --- a/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java +++ b/src/main/java/world/bentobox/limits/listeners/BlockLimitsListener.java @@ -52,6 +52,7 @@ import world.bentobox.bentobox.database.Database; import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; +import world.bentobox.limits.DisplayNames; import world.bentobox.limits.Limits; import world.bentobox.limits.Settings; import world.bentobox.limits.objects.IslandBlockCount; @@ -307,7 +308,7 @@ private void notify(Cancellable e, User user, int limit, Material m) { if (limit > -1) { if (addon.getSettings().isShowLimitMessages()) { user.notify("block-limits.hit-limit", - "[material]", Util.prettifyText(m.toString()), + "[material]", DisplayNames.material(user, m.getKey()), TextVariables.NUMBER, String.valueOf(limit)); } e.setCancelled(true); diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index e6f91f9..7226811 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -28,6 +28,7 @@ import world.bentobox.bentobox.database.objects.Island; import world.bentobox.bentobox.util.Util; import world.bentobox.limits.EntityGroup; +import world.bentobox.limits.DisplayNames; import world.bentobox.limits.Limits; import world.bentobox.limits.Settings; import world.bentobox.limits.objects.IslandBlockCount; @@ -151,15 +152,16 @@ public void onBlock(HangingPlaceEvent hangingPlaceEvent) { if (!addon.getSettings().isShowLimitMessages()) { return; } + User u = User.getInstance(player); if (res.getTypelimit() != null) { - User.getInstance(player).notify("block-limits.hit-limit", "[material]", - Util.prettifyText(hangingPlaceEvent.getEntity().getType().toString()), + u.notify("block-limits.hit-limit", "[material]", + DisplayNames.entity(u, hangingPlaceEvent.getEntity().getType()), TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { - User.getInstance(player).notify("block-limits.hit-limit", "[material]", + u.notify("block-limits.hit-limit", "[material]", res.getGrouplimit().getKey().getName() + " (" + res.getGrouplimit().getKey().getTypes().stream() - .map(x -> Util.prettifyText(x.toString())) + .map(x -> DisplayNames.entity(u, x)) .collect(Collectors.joining(", ")) + ")", TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue())); @@ -235,15 +237,16 @@ private void notifyEntityLimit(Player player, EntityType type, AtLimitResult res if (!addon.getSettings().isShowLimitMessages()) { return; } + User u = User.getInstance(player); if (res.getTypelimit() != null) { - User.getInstance(player).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, - Util.prettifyText(type.toString()), TextVariables.NUMBER, + u.notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, + DisplayNames.entity(u, type), TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { - User.getInstance(player).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, + u.notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, res.getGrouplimit().getKey().getName() + " (" + res.getGrouplimit().getKey().getTypes().stream() - .map(x -> Util.prettifyText(x.toString())) + .map(x -> DisplayNames.entity(u, x)) .collect(Collectors.joining(", ")) + ")", TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue())); @@ -568,15 +571,16 @@ private void tellPlayers(Location location, Entity entity, SpawnReason spawnReas for (Entity ent : w.getNearbyEntities(location, 5, 5, 5)) { if (ent instanceof Player p) { p.updateInventory(); + User u = User.getInstance(p); if (res.getTypelimit() != null) { - User.getInstance(p).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, - Util.prettifyText(entity.getType().toString()), + u.notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, + DisplayNames.entity(u, entity.getType()), TextVariables.NUMBER, String.valueOf(res.getTypelimit().getValue())); } else { - User.getInstance(p).notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, + u.notify(ENTITY_LIMIT_HIT, ENTITY_PLACEHOLDER, res.getGrouplimit().getKey().getName() + " (" + res.getGrouplimit().getKey().getTypes().stream() - .map(x -> Util.prettifyText(x.toString())) + .map(x -> DisplayNames.entity(u, x)) .collect(Collectors.joining(", ")) + ")", TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue())); diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index 477425b..4fea8dc 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -1,69 +1,76 @@ -########################################################################################### -# This is a YML file. Be careful when editing. Check your edits in a YAML checker like # -# the one at http://yaml-online-parser.appspot.com # -########################################################################################### - -block-limits: - hit-limit: "[material] limited to [number]!" -entity-limits: - hit-limit: "[entity] spawning limited to [number]!" -limits: - panel-title: "Island limits" - -admin: - limits: - main: - parameters: "" - description: "show the island limits for player" - calc: - parameters: "" - description: "recalculate the island limits for player" - finished: " Island recalc finished successfully!" - offset: - unknown: " Unknown material or entity [name]." - description: "allows to manage limits offsets for materials and entities" - set: - parameters: " " - description: "sets new offset for material or entity limit" - success: " Limit offset for [name] is set to [number]." - same: " Limit offset for [name] is already [number]." - add: - parameters: " " - description: "adds offset for material or entity limit" - success: " Limit offset for [name] is increased till [number]." - remove: - parameters: " " - description: "reduces offset for material or entity limit" - success: " Limit offset for [name] is reduced till [number]." - reset: - parameters: " " - description: "removes offset for material or entity" - success: " Limit offset for [name] is set to 0." - view: - parameters: " " - description: "displays offset for material or entity" - message: " [name] offset is set to [number]." -island: - limits: - description: "show your island limits" - max-color: "" - regular-color: "" - block-limit-syntax: "[number]/[limit]" - no-limits: " No limits set in this world" - panel: - title-syntax: '[title] [env]' - entity-group-name-syntax: '[name]' - entity-name-syntax: '[name]' - block-name-syntax: '[name]' - env-overworld: "Overworld" - env-nether: "Nether" - env-end: "End" - errors: - no-owner: " That island has no owner" - not-on-island: " This location does not have limits set." - recount: - description: "recounts limits for your island" - now-recounting: " Now recounting. This could take a while, please wait..." - in-progress: " Island recound is in progress. Please wait..." - time-out: " Time out when recounting. Is the island really big?" - +########################################################################################### +# This is a YML file. Be careful when editing. Check your edits in a YAML checker like # +# the one at http://yaml-online-parser.appspot.com # +########################################################################################### + +block-limits: + hit-limit: "[material] limited to [number]!" +entity-limits: + hit-limit: "[entity] spawning limited to [number]!" +limits: + panel-title: "Island limits" + +admin: + limits: + main: + parameters: "" + description: "show the island limits for player" + calc: + parameters: "" + description: "recalculate the island limits for player" + finished: " Island recalc finished successfully!" + offset: + unknown: " Unknown material or entity [name]." + description: "allows to manage limits offsets for materials and entities" + set: + parameters: " " + description: "sets new offset for material or entity limit" + success: " Limit offset for [name] is set to [number]." + same: " Limit offset for [name] is already [number]." + add: + parameters: " " + description: "adds offset for material or entity limit" + success: " Limit offset for [name] is increased till [number]." + remove: + parameters: " " + description: "reduces offset for material or entity limit" + success: " Limit offset for [name] is reduced till [number]." + reset: + parameters: " " + description: "removes offset for material or entity" + success: " Limit offset for [name] is set to 0." + view: + parameters: " " + description: "displays offset for material or entity" + message: " [name] offset is set to [number]." +island: + limits: + description: "show your island limits" + # Optional manual name translations, used in the limits GUI and hit-limit messages. + # Keys are lowercase Bukkit Material / EntityType names. Anything not listed falls + # back to the automatic English name, so these sections can be left empty. + #materials: + # hopper: "Hopper" + #entities: + # enderman: "Enderman" + max-color: "" + regular-color: "" + block-limit-syntax: "[number]/[limit]" + no-limits: " No limits set in this world" + panel: + title-syntax: '[title] [env]' + entity-group-name-syntax: '[name]' + entity-name-syntax: '[name]' + block-name-syntax: '[name]' + env-overworld: "Overworld" + env-nether: "Nether" + env-end: "End" + errors: + no-owner: " That island has no owner" + not-on-island: " This location does not have limits set." + recount: + description: "recounts limits for your island" + now-recounting: " Now recounting. This could take a while, please wait..." + in-progress: " Island recound is in progress. Please wait..." + time-out: " Time out when recounting. Is the island really big?" + diff --git a/src/test/java/world/bentobox/limits/DisplayNamesTest.java b/src/test/java/world/bentobox/limits/DisplayNamesTest.java new file mode 100644 index 0000000..15e3188 --- /dev/null +++ b/src/test/java/world/bentobox/limits/DisplayNamesTest.java @@ -0,0 +1,50 @@ +package world.bentobox.limits; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import world.bentobox.bentobox.api.user.User; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class DisplayNamesTest { + + @Mock + private User user; + + @Test + void testMaterialTranslated() { + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + when(user.getTranslationOrNothing("island.limits.materials.hopper")).thenReturn("Trichter"); + assertEquals("Trichter", DisplayNames.material(user, Material.HOPPER.getKey())); + } + + @Test + void testMaterialFallsBackToPrettyName() { + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + assertEquals("Hopper", DisplayNames.material(user, Material.HOPPER.getKey())); + } + + @Test + void testEntityTranslated() { + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + when(user.getTranslationOrNothing("island.limits.entities.enderman")).thenReturn("Enderman (DE)"); + assertEquals("Enderman (DE)", DisplayNames.entity(user, EntityType.ENDERMAN)); + } + + @Test + void testEntityFallsBackToPrettyName() { + when(user.getTranslationOrNothing(anyString())).thenReturn(""); + assertEquals("Enderman", DisplayNames.entity(user, EntityType.ENDERMAN)); + } +}