Skip to content

Commit bb9fa98

Browse files
f0reachARRclaude
andcommitted
feat(ui): render item names as translatable components with item hover
Dialog bodies, trade result messages and owner notifications printed the raw Material enum name (DIAMOND_SWORD), which is neither localized nor inspectable. Centralize item-name rendering in Displays#itemName: custom display name when the stack has one, otherwise a translatable of the item's translation key (material name as client fallback), always with the stack attached as an item hover. TradeNotifier previously plain-text serialized the custom display name and passed it through Placeholder.parsed, so an item renamed with MiniMessage syntax was parsed as tags; it now uses Placeholder.component. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 76dd4fb commit bb9fa98

5 files changed

Lines changed: 119 additions & 26 deletions

File tree

src/main/java/me/f0reach/vshop/shop/edit/SlotEditFlow.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import me.f0reach.vshop.model.ShopSlot;
1111
import me.f0reach.vshop.model.TradeSide;
1212
import me.f0reach.vshop.ui.dialog.DialogService;
13+
import me.f0reach.vshop.ui.text.Displays;
1314
import net.kyori.adventure.text.Component;
1415
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
1516
import org.bukkit.entity.Player;
@@ -75,7 +76,7 @@ private void showHub(Player editor, Shop shop, ShopSlot existing, int slotIndex,
7576
Component title = messages.get(existing == null
7677
? "edit.slot.hub.create-title" : "edit.slot.hub.edit-title");
7778
Component body = messages.get("edit.slot.hub.body",
78-
Placeholder.parsed("item", template.getType().name()),
79+
Placeholder.component("item", Displays.itemName(template)),
7980
Placeholder.component("side", sideLabel(working.side)));
8081

8182
List<DialogService.ButtonSpec> buttons = new ArrayList<>();
@@ -133,7 +134,7 @@ private void showSellForm(Player editor, Shop shop, ShopSlot existing, int slotI
133134
int unitMax = Math.max(1, template.getMaxStackSize());
134135
Component title = messages.get("edit.slot.sell-title");
135136
Component body = messages.get("edit.slot.body",
136-
Placeholder.parsed("item", template.getType().name()),
137+
Placeholder.component("item", Displays.itemName(template)),
137138
Placeholder.parsed("unit_max", Integer.toString(unitMax)));
138139
Component submit = messages.get("edit.slot.submit");
139140

@@ -206,7 +207,7 @@ private void showBuyForm(Player editor, Shop shop, ShopSlot existing, int slotIn
206207
int unitMax = Math.max(1, template.getMaxStackSize());
207208
Component title = messages.get("edit.slot.buy-title");
208209
Component body = messages.get("edit.slot.body",
209-
Placeholder.parsed("item", template.getType().name()),
210+
Placeholder.component("item", Displays.itemName(template)),
210211
Placeholder.parsed("unit_max", Integer.toString(unitMax)));
211212
Component submit = messages.get("edit.slot.submit");
212213

@@ -271,7 +272,7 @@ private void confirmDelete(Player editor, ShopSlot slot, Runnable onDone, Runnab
271272
dialogs.confirmOnce(editor,
272273
messages.get("edit.delete.title"),
273274
messages.get("edit.delete.body",
274-
Placeholder.parsed("item", slot.itemTemplate().getType().name())),
275+
Placeholder.component("item", Displays.itemName(slot.itemTemplate()))),
275276
messages.get("edit.delete.yes"),
276277
messages.get("edit.delete.no"),
277278
() -> {

src/main/java/me/f0reach/vshop/shop/trade/TradeFlow.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import me.f0reach.vshop.sound.SoundService;
1515
import me.f0reach.vshop.storage.StorageManager;
1616
import me.f0reach.vshop.ui.dialog.DialogService;
17+
import me.f0reach.vshop.ui.text.Displays;
1718
import net.kyori.adventure.text.Component;
1819
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
1920
import org.bukkit.entity.Player;
@@ -347,11 +348,6 @@ private void execute(Player viewer, Shop shop, ShopSlot slot, TradeSide side,
347348
}
348349

349350
private Component displayName(ShopSlot slot) {
350-
var meta = slot.itemTemplate().getItemMeta();
351-
if (meta != null && meta.hasDisplayName()) {
352-
var dn = meta.displayName();
353-
if (dn != null) return dn;
354-
}
355-
return Component.text(slot.itemTemplate().getType().name());
351+
return Displays.itemName(slot.itemTemplate());
356352
}
357353
}

src/main/java/me/f0reach/vshop/shop/trade/TradeNotifier.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import me.f0reach.vshop.model.TradeSide;
77
import me.f0reach.vshop.storage.StorageManager;
88
import me.f0reach.vshop.economy.EconomyService;
9+
import me.f0reach.vshop.ui.text.Displays;
910
import net.kyori.adventure.text.Component;
1011
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
1112
import org.bukkit.Bukkit;
@@ -49,7 +50,7 @@ public void notifyOnline(List<CoOwner> coOwners, Shop shop, TradeSide side, int
4950
Placeholder.parsed("shop_name", shop.name()),
5051
Placeholder.parsed("amount", Integer.toString(amount)),
5152
Placeholder.parsed("price", economy.format(value)),
52-
Placeholder.parsed("item", itemName(item))
53+
Placeholder.component("item", Displays.itemName(item))
5354
);
5455
p.sendMessage(msg);
5556
}
@@ -99,14 +100,4 @@ public void flushPending(Player player) {
99100
});
100101
}
101102

102-
private String itemName(ItemStack item) {
103-
if (item == null || !item.hasItemMeta()) return item == null ? "?" : item.getType().name();
104-
var meta = item.getItemMeta();
105-
if (meta != null && meta.hasDisplayName()) {
106-
var dn = meta.displayName();
107-
if (dn != null) return net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
108-
.plainText().serialize(dn);
109-
}
110-
return item.getType().name();
111-
}
112103
}

src/main/java/me/f0reach/vshop/ui/text/Displays.java

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import net.kyori.adventure.text.event.HoverEvent;
55
import net.kyori.adventure.text.format.NamedTextColor;
66
import org.bukkit.inventory.ItemStack;
7+
import org.bukkit.inventory.meta.ItemMeta;
78

89
import java.util.UUID;
910

@@ -18,16 +19,65 @@ private Displays() {}
1819

1920
/**
2021
* A translatable item name (so vanilla / resource-pack overrides apply per
21-
* client locale) with a Minecraft-style item hover. Falls back to the
22-
* material key when {@link ItemStack#displayName()} would throw.
22+
* client locale) with a Minecraft-style item hover. Rendered the vanilla
23+
* chat way — bracketed and carrying the stack's rarity style. Falls back to
24+
* {@link #itemName(ItemStack)} when {@link ItemStack#displayName()} would
25+
* throw.
2326
*/
2427
public static Component item(ItemStack stack) {
2528
if (stack == null) return Component.text("?");
2629
try {
27-
return stack.displayName().hoverEvent(stack.asHoverEvent());
30+
return withItemHover(stack.displayName(), stack);
2831
} catch (Throwable ignored) {
29-
return Component.text(stack.getType().name())
30-
.hoverEvent(stack.asHoverEvent());
32+
return itemName(stack);
33+
}
34+
}
35+
36+
/**
37+
* The bare item name for embedding into a localized message: the stack's
38+
* custom display name when it has one, otherwise a
39+
* {@link Component#translatable} of the item's translation key so every
40+
* client renders it in its own language. The stack itself is attached as an
41+
* item hover, so the full tooltip stays reachable.
42+
*
43+
* <p>Unlike {@link #item(ItemStack)} this carries no brackets and no rarity
44+
* color, which keeps the surrounding message in control of the styling —
45+
* insert it with {@code Placeholder.component("item", ...)}.</p>
46+
*/
47+
public static Component itemName(ItemStack stack) {
48+
if (stack == null) return Component.text("?");
49+
return withItemHover(baseName(stack), stack);
50+
}
51+
52+
/**
53+
* Custom display name if the stack carries one, else a translatable of the
54+
* item's translation key with the material name as the client-side
55+
* fallback. Both lookups need the server implementation, so both are
56+
* guarded — the last resort is the plain material name.
57+
*/
58+
private static Component baseName(ItemStack stack) {
59+
try {
60+
ItemMeta meta = stack.getItemMeta();
61+
if (meta != null && meta.hasDisplayName()) {
62+
Component displayName = meta.displayName();
63+
if (displayName != null) return displayName;
64+
}
65+
} catch (Throwable ignored) {
66+
// fall through to the translatable name
67+
}
68+
try {
69+
return Component.translatable(stack.translationKey(), stack.getType().name());
70+
} catch (Throwable ignored) {
71+
return Component.text(stack.getType().name());
72+
}
73+
}
74+
75+
/** Item hover is implementation-backed; drop it rather than fail the message. */
76+
private static Component withItemHover(Component name, ItemStack stack) {
77+
try {
78+
return name.hoverEvent(stack.asHoverEvent());
79+
} catch (Throwable ignored) {
80+
return name;
3181
}
3282
}
3383

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package me.f0reach.vshop.ui.text;
2+
3+
import me.f0reach.vshop.testsupport.BukkitTestSupport;
4+
import net.kyori.adventure.text.Component;
5+
import net.kyori.adventure.text.TranslatableComponent;
6+
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
7+
import org.bukkit.Material;
8+
import org.bukkit.inventory.ItemStack;
9+
import org.bukkit.inventory.meta.ItemMeta;
10+
import org.junit.jupiter.api.BeforeAll;
11+
import org.junit.jupiter.api.Test;
12+
13+
import static org.junit.jupiter.api.Assertions.assertEquals;
14+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
15+
import static org.junit.jupiter.api.Assertions.assertNotNull;
16+
17+
class DisplaysTest {
18+
19+
@BeforeAll
20+
static void boot() {
21+
BukkitTestSupport.ensureBukkit();
22+
}
23+
24+
@Test
25+
void rendersPlainItemAsTranslatableName() {
26+
ItemStack stack = BukkitTestSupport.item(Material.DIAMOND);
27+
28+
Component rendered = Displays.itemName(stack);
29+
30+
TranslatableComponent translatable = assertInstanceOf(TranslatableComponent.class, rendered);
31+
assertEquals("item.minecraft.diamond", translatable.key());
32+
// Clients that don't know the key (and plain-text consumers such as the
33+
// Bedrock bridge) fall back to the material name.
34+
assertEquals(Material.DIAMOND.name(), translatable.fallback());
35+
}
36+
37+
@Test
38+
void keepsCustomDisplayNameWhenPresent() {
39+
ItemStack stack = BukkitTestSupport.item(Material.DIAMOND);
40+
ItemMeta meta = stack.getItemMeta();
41+
meta.displayName(Component.text("Shiny"));
42+
stack.setItemMeta(meta);
43+
44+
Component rendered = Displays.itemName(stack);
45+
46+
assertEquals("Shiny", PlainTextComponentSerializer.plainText().serialize(rendered));
47+
}
48+
49+
@Test
50+
void nullStackRendersPlaceholder() {
51+
assertEquals("?", PlainTextComponentSerializer.plainText()
52+
.serialize(Displays.itemName(null)));
53+
assertNotNull(Displays.item(null));
54+
}
55+
}

0 commit comments

Comments
 (0)