diff --git a/connector/pom.xml b/connector/pom.xml
index 755828467..483afad1d 100644
--- a/connector/pom.xml
+++ b/connector/pom.xml
@@ -228,6 +228,12 @@
             <version>${adventure.version}</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>net.kyori</groupId>
+            <artifactId>adventure-text-serializer-plain</artifactId>
+            <version>${adventure.version}</version>
+            <scope>compile</scope>
+        </dependency>
         <!-- Other -->
         <dependency>
             <groupId>junit</groupId>
diff --git a/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java b/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java
index 71b5cbda9..10451772c 100644
--- a/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java
+++ b/connector/src/main/java/org/geysermc/connector/inventory/AnvilContainer.java
@@ -26,12 +26,49 @@
 package org.geysermc.connector.inventory;
 
 import com.github.steveice10.mc.protocol.data.game.window.WindowType;
+import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
+import lombok.Getter;
+import lombok.Setter;
 
 /**
- * Used to determine if rename packets should be sent.
+ * Used to determine if rename packets should be sent and stores
+ * the expected level cost for AnvilInventoryUpdater
  */
+@Getter @Setter
 public class AnvilContainer extends Container {
+    /**
+     * Stores the level cost received as a window property from Java
+     */
+    private int javaLevelCost = 0;
+    /**
+     * A flag to specify whether javaLevelCost can be used as it can
+     * be outdated or not sent at all.
+     */
+    private boolean useJavaLevelCost = false;
+
+    /**
+     * The new name of the item as received from Bedrock
+     */
+    private String newName = null;
+
+    private GeyserItemStack lastInput = GeyserItemStack.EMPTY;
+    private GeyserItemStack lastMaterial = GeyserItemStack.EMPTY;
+
+    private int lastTargetSlot = -1;
+
     public AnvilContainer(String title, int id, int size, WindowType windowType, PlayerInventory playerInventory) {
         super(title, id, size, windowType, playerInventory);
     }
+
+    public GeyserItemStack getInput() {
+        return getItem(0);
+    }
+
+    public GeyserItemStack getMaterial() {
+        return getItem(1);
+    }
+
+    public GeyserItemStack getResult() {
+        return getItem(2);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java
index 681ca432c..354280c08 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/bedrock/BedrockFilterTextTranslator.java
@@ -32,6 +32,8 @@ import org.geysermc.connector.inventory.CartographyContainer;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.PacketTranslator;
 import org.geysermc.connector.network.translators.Translator;
+import org.geysermc.connector.network.translators.chat.MessageTranslator;
+import org.geysermc.connector.utils.ItemUtils;
 
 /**
  * Used to send strings to the server and filter out unwanted words.
@@ -47,12 +49,31 @@ public class BedrockFilterTextTranslator extends PacketTranslator<FilterTextPack
             return;
         }
         packet.setFromServer(true);
-        session.sendUpstreamPacket(packet);
+        if (session.getOpenInventory() instanceof AnvilContainer anvilContainer) {
+            anvilContainer.setNewName(packet.getText());
 
-        if (session.getOpenInventory() instanceof AnvilContainer) {
-            // Java Edition sends a packet every time an item is renamed even slightly in GUI. Fortunately, this works out for us now
-            ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(packet.getText());
-            session.sendDownstreamPacket(renameItemPacket);
+            String originalName = ItemUtils.getCustomName(anvilContainer.getInput().getNbt());
+
+            String plainOriginalName = MessageTranslator.convertToPlainText(originalName, session.getLocale());
+            String plainNewName = MessageTranslator.convertToPlainText(packet.getText(), session.getLocale());
+            if (!plainOriginalName.equals(plainNewName)) {
+                // Strip out formatting since Java Edition does not allow it
+                packet.setText(plainNewName);
+                // Java Edition sends a packet every time an item is renamed even slightly in GUI. Fortunately, this works out for us now
+                ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(plainNewName);
+                session.sendDownstreamPacket(renameItemPacket);
+            } else {
+                // Restore formatting for item since we're not renaming
+                packet.setText(MessageTranslator.convertMessageLenient(originalName));
+                // Java Edition sends the original custom name when not renaming,
+                // if there isn't a custom name an empty string is sent
+                ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(plainOriginalName);
+                session.sendDownstreamPacket(renameItemPacket);
+            }
+
+            anvilContainer.setUseJavaLevelCost(false);
+            session.getInventoryTranslator().updateSlot(session, anvilContainer, 1);
         }
+        session.sendUpstreamPacket(packet);
     }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java
index 520e27455..55087de0f 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/chat/MessageTranslator.java
@@ -31,6 +31,7 @@ import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
 import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
 import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
 import org.geysermc.connector.GeyserConnector;
 import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.utils.LanguageUtils;
@@ -179,6 +180,33 @@ public class MessageTranslator {
         return GSON_SERIALIZER.serialize(component);
     }
 
+    /**
+     * Convert JSON and legacy format message to plain text
+     *
+     * @param message Message to convert
+     * @param locale Locale to use for translation strings
+     * @return The plain text of the message
+     */
+    public static String convertToPlainText(String message, String locale) {
+        if (message == null) {
+            return "";
+        }
+        Component messageComponent = null;
+        if (message.startsWith("{") && message.endsWith("}")) {
+            // Message is a JSON object
+            try {
+                messageComponent = GSON_SERIALIZER.deserialize(message);
+                // Translate any components that require it
+                messageComponent = RENDERER.render(messageComponent, locale);
+            } catch (Exception ignored) {
+            }
+        }
+        if (messageComponent == null) {
+            messageComponent = LegacyComponentSerializer.legacySection().deserialize(message);
+        }
+        return PlainTextComponentSerializer.plainText().serialize(messageComponent);
+    }
+
     /**
      * Convert a team color to a chat color
      *
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java
index 9aef64b36..b638d892a 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/translators/AnvilInventoryTranslator.java
@@ -32,12 +32,13 @@ import com.nukkitx.protocol.bedrock.data.inventory.StackRequestSlotInfoData;
 import org.geysermc.connector.inventory.AnvilContainer;
 import org.geysermc.connector.inventory.Inventory;
 import org.geysermc.connector.inventory.PlayerInventory;
+import org.geysermc.connector.network.session.GeyserSession;
 import org.geysermc.connector.network.translators.inventory.BedrockContainerSlot;
-import org.geysermc.connector.network.translators.inventory.updater.UIInventoryUpdater;
+import org.geysermc.connector.network.translators.inventory.updater.AnvilInventoryUpdater;
 
 public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator {
     public AnvilInventoryTranslator() {
-        super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL, UIInventoryUpdater.INSTANCE,
+        super(3, "minecraft:anvil[facing=north]", ContainerType.ANVIL, AnvilInventoryUpdater.INSTANCE,
                 "minecraft:chipped_anvil", "minecraft:damaged_anvil");
     }
 
@@ -75,4 +76,14 @@ public class AnvilInventoryTranslator extends AbstractBlockInventoryTranslator {
     public Inventory createInventory(String name, int windowId, WindowType windowType, PlayerInventory playerInventory) {
         return new AnvilContainer(name, windowId, this.size, windowType, playerInventory);
     }
+
+    @Override
+    public void updateProperty(GeyserSession session, Inventory inventory, int key, int value) {
+        // The only property sent by Java is key 0 which is the level cost
+        if (key != 0) return;
+        AnvilContainer anvilContainer = (AnvilContainer) inventory;
+        anvilContainer.setJavaLevelCost(value);
+        anvilContainer.setUseJavaLevelCost(true);
+        updateSlot(session, anvilContainer, 1);
+    }
 }
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/AnvilInventoryUpdater.java b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/AnvilInventoryUpdater.java
new file mode 100644
index 000000000..01e5cbebc
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/inventory/updater/AnvilInventoryUpdater.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.network.translators.inventory.updater;
+
+import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
+import com.github.steveice10.mc.protocol.packet.ingame.client.window.ClientRenameItemPacket;
+import com.github.steveice10.opennbt.tag.builtin.*;
+import com.nukkitx.nbt.NbtMap;
+import com.nukkitx.nbt.NbtMapBuilder;
+import com.nukkitx.protocol.bedrock.data.inventory.ContainerId;
+import com.nukkitx.protocol.bedrock.data.inventory.ItemData;
+import com.nukkitx.protocol.bedrock.packet.InventorySlotPacket;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMaps;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.inventory.AnvilContainer;
+import org.geysermc.connector.inventory.GeyserItemStack;
+import org.geysermc.connector.inventory.Inventory;
+import org.geysermc.connector.network.session.GeyserSession;
+import org.geysermc.connector.network.translators.chat.MessageTranslator;
+import org.geysermc.connector.network.translators.inventory.InventoryTranslator;
+import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
+import org.geysermc.connector.registry.Registries;
+import org.geysermc.connector.registry.type.EnchantmentData;
+import org.geysermc.connector.utils.ItemUtils;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class AnvilInventoryUpdater extends InventoryUpdater {
+    public static final AnvilInventoryUpdater INSTANCE = new AnvilInventoryUpdater();
+
+    private static final int MAX_LEVEL_COST = 40;
+
+    @Override
+    public void updateInventory(InventoryTranslator translator, GeyserSession session, Inventory inventory) {
+        super.updateInventory(translator, session, inventory);
+        AnvilContainer anvilContainer = (AnvilContainer) inventory;
+        updateInventoryState(session, anvilContainer);
+        int targetSlot = getTargetSlot(session, anvilContainer);
+        for (int i = 0; i < translator.size; i++) {
+            final int bedrockSlot = translator.javaSlotToBedrock(i);
+            if (bedrockSlot == 50)
+                continue;
+            if (i == targetSlot) {
+                updateTargetSlot(translator, session, anvilContainer, targetSlot);
+            } else {
+                InventorySlotPacket slotPacket = new InventorySlotPacket();
+                slotPacket.setContainerId(ContainerId.UI);
+                slotPacket.setSlot(bedrockSlot);
+                slotPacket.setItem(inventory.getItem(i).getItemData(session));
+                session.sendUpstreamPacket(slotPacket);
+            }
+        }
+    }
+
+    @Override
+    public boolean updateSlot(InventoryTranslator translator, GeyserSession session, Inventory inventory, int javaSlot) {
+        if (super.updateSlot(translator, session, inventory, javaSlot))
+            return true;
+        AnvilContainer anvilContainer = (AnvilContainer) inventory;
+        updateInventoryState(session, anvilContainer);
+
+        int lastTargetSlot = anvilContainer.getLastTargetSlot();
+        int targetSlot = getTargetSlot(session, anvilContainer);
+        if (targetSlot != javaSlot) {
+            // Update the requested slot
+            InventorySlotPacket slotPacket = new InventorySlotPacket();
+            slotPacket.setContainerId(ContainerId.UI);
+            slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
+            slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+            session.sendUpstreamPacket(slotPacket);
+        } else if (lastTargetSlot != javaSlot) {
+            // Update the previous target slot to remove repair cost changes
+            InventorySlotPacket slotPacket = new InventorySlotPacket();
+            slotPacket.setContainerId(ContainerId.UI);
+            slotPacket.setSlot(translator.javaSlotToBedrock(lastTargetSlot));
+            slotPacket.setItem(inventory.getItem(lastTargetSlot).getItemData(session));
+            session.sendUpstreamPacket(slotPacket);
+        }
+
+        updateTargetSlot(translator, session, anvilContainer, targetSlot);
+        return true;
+    }
+
+    private void updateInventoryState(GeyserSession session, AnvilContainer anvilContainer) {
+        GeyserItemStack input = anvilContainer.getInput();
+        if (!input.equals(anvilContainer.getLastInput())) {
+            anvilContainer.setLastInput(input.copy());
+            anvilContainer.setUseJavaLevelCost(false);
+
+            // Changing the item in the input slot resets the name field on Bedrock, but
+            // does not result in a FilterTextPacket
+            String originalName = MessageTranslator.convertToPlainText(ItemUtils.getCustomName(input.getNbt()), session.getLocale());
+            ClientRenameItemPacket renameItemPacket = new ClientRenameItemPacket(originalName);
+            session.sendDownstreamPacket(renameItemPacket);
+
+            anvilContainer.setNewName(null);
+        }
+
+        GeyserItemStack material = anvilContainer.getMaterial();
+        if (!material.equals(anvilContainer.getLastMaterial())) {
+            anvilContainer.setLastMaterial(material.copy());
+            anvilContainer.setUseJavaLevelCost(false);
+        }
+    }
+
+    /**
+     * @param anvilContainer the anvil inventory
+     * @return the slot to change the repair cost
+     */
+    private int getTargetSlot(GeyserSession session, AnvilContainer anvilContainer) {
+        GeyserItemStack input = anvilContainer.getInput();
+        GeyserItemStack material = anvilContainer.getMaterial();
+
+        if (!material.isEmpty()) {
+            if (!input.isEmpty() && isRepairing(session, input, material)) {
+                // Changing the repair cost on the material item makes it non-stackable
+                return 0;
+            }
+            // Prefer changing the material item because it does not reset the name field
+            return 1;
+        }
+        return 0;
+    }
+
+    private void updateTargetSlot(InventoryTranslator translator, GeyserSession session, AnvilContainer anvilContainer, int slot) {
+        ItemData itemData = anvilContainer.getItem(slot).getItemData(session);
+        itemData = hijackRepairCost(session, anvilContainer, itemData);
+
+        if (slot == 0 && isRenaming(session, anvilContainer, true)) {
+            // Can't change the RepairCost because it resets the name field on Bedrock
+            return;
+        }
+
+        anvilContainer.setLastTargetSlot(slot);
+
+        InventorySlotPacket slotPacket = new InventorySlotPacket();
+        slotPacket.setContainerId(ContainerId.UI);
+        slotPacket.setSlot(translator.javaSlotToBedrock(slot));
+        slotPacket.setItem(itemData);
+        session.sendUpstreamPacket(slotPacket);
+    }
+
+    private ItemData hijackRepairCost(GeyserSession session, AnvilContainer anvilContainer, ItemData itemData) {
+        if (itemData.isNull()) {
+            return itemData;
+        }
+        // Fix level count by adjusting repair cost
+        int newRepairCost;
+        if (anvilContainer.isUseJavaLevelCost()) {
+            newRepairCost = anvilContainer.getJavaLevelCost();
+        } else {
+            // Did not receive a ServerWindowPropertyPacket with the level cost
+            newRepairCost = calcLevelCost(session, anvilContainer, false);
+        }
+
+        int bedrockLevelCost = calcLevelCost(session, anvilContainer, true);
+        if (bedrockLevelCost == -1) {
+            // Bedrock is unable to combine/repair the items
+            return itemData;
+        }
+
+        newRepairCost -= bedrockLevelCost;
+        if (newRepairCost == 0) {
+            // No change to the repair cost needed
+            return itemData;
+        }
+
+        NbtMapBuilder tagBuilder = NbtMap.builder();
+        if (itemData.getTag() != null) {
+            newRepairCost += itemData.getTag().getInt("RepairCost", 0);
+            tagBuilder.putAll(itemData.getTag());
+        }
+        tagBuilder.put("RepairCost", newRepairCost);
+        return itemData.toBuilder().tag(tagBuilder.build()).build();
+    }
+
+    /**
+     * Calculate the number of levels needed to combine/rename an item
+     *
+     * @param session        the geyser session
+     * @param anvilContainer the anvil container
+     * @param bedrock        True to count enchantments like Bedrock
+     * @return the number of levels needed
+     */
+    public int calcLevelCost(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) {
+        GeyserItemStack input = anvilContainer.getInput();
+        GeyserItemStack material = anvilContainer.getMaterial();
+
+        if (input.isEmpty()) {
+            return 0;
+        }
+        int totalRepairCost = getRepairCost(input);
+        int cost = 0;
+        if (!material.isEmpty()) {
+            totalRepairCost += getRepairCost(material);
+            if (isCombining(session, input, material)) {
+                if (hasDurability(session, input) && input.getJavaId() == material.getJavaId()) {
+                    cost += calcMergeRepairCost(session, input, material);
+                }
+
+                int enchantmentLevelCost = calcMergeEnchantmentCost(session, input, material, bedrock);
+                if (enchantmentLevelCost != -1) {
+                    cost += enchantmentLevelCost;
+                } else if (cost == 0) {
+                    // Can't repair or merge enchantments
+                    return -1;
+                }
+            } else if (hasDurability(session, input) && isRepairing(session, input, material)) {
+                cost = calcRepairLevelCost(session, input, material);
+                if (cost == -1) {
+                    // No damage to repair
+                    return -1;
+                }
+            } else {
+                return -1;
+            }
+        }
+
+        int totalCost = totalRepairCost + cost;
+        if (isRenaming(session, anvilContainer, bedrock)) {
+            totalCost++;
+            if (cost == 0 && totalCost >= MAX_LEVEL_COST) {
+                // Items can still be renamed when the level cost for renaming exceeds 40
+                totalCost = MAX_LEVEL_COST - 1;
+            }
+        }
+        return totalCost;
+    }
+
+    /**
+     * Calculate the levels needed to repair an item with its repair material
+     * E.g. iron_sword + iron_ingot
+     *
+     * @param session  Geyser session
+     * @param input    an item with durability
+     * @param material the item's respective repair material
+     * @return the number of levels needed or 0 if it is not possible to repair any further
+     */
+    private int calcRepairLevelCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+        int newDamage = getDamage(input);
+        int unitRepair = Math.min(newDamage, input.getMapping(session).getMaxDamage() / 4);
+        if (unitRepair <= 0) {
+            // No damage to repair
+            return -1;
+        }
+        for (int i = 0; i < material.getAmount(); i++) {
+            newDamage -= unitRepair;
+            unitRepair = Math.min(newDamage, input.getMapping(session).getMaxDamage() / 4);
+            if (unitRepair <= 0) {
+                return i + 1;
+            }
+        }
+        return material.getAmount();
+    }
+
+    /**
+     * Calculate the levels cost for repairing items by combining two of the same item
+     *
+     * @param session  Geyser session
+     * @param input    an item with durability
+     * @param material a matching item
+     * @return the number of levels needed or 0 if it is not possible to repair any further
+     */
+    private int calcMergeRepairCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+        // If the material item is damaged 112% or more, then the input item will not be repaired
+        if (getDamage(input) > 0 && getDamage(material) < (material.getMapping(session).getMaxDamage() * 112 / 100)) {
+            return 2;
+        }
+        return 0;
+    }
+
+    /**
+     * Calculate the levels needed for combining the enchantments of two items
+     *
+     * @param session  Geyser session
+     * @param input    an item with durability
+     * @param material a matching item
+     * @param bedrock  True to count enchantments like Bedrock, False to count like Java
+     * @return the number of levels needed or -1 if no enchantments can be applied
+     */
+    private int calcMergeEnchantmentCost(GeyserSession session, GeyserItemStack input, GeyserItemStack material, boolean bedrock) {
+        boolean hasCompatible = false;
+        Object2IntMap<JavaEnchantment> combinedEnchantments = getEnchantments(session, input, bedrock);
+        int cost = 0;
+        for (Object2IntMap.Entry<JavaEnchantment> entry : getEnchantments(session, material, bedrock).object2IntEntrySet()) {
+            JavaEnchantment enchantment = entry.getKey();
+            EnchantmentData data = Registries.ENCHANTMENTS.get(enchantment);
+            if (data == null) {
+                GeyserConnector.getInstance().getLogger().debug("Java enchantment not in registry: " + enchantment);
+                continue;
+            }
+
+            boolean canApply = isEnchantedBook(session, input) || data.validItems().contains(input.getJavaId());
+            for (JavaEnchantment incompatible : data.incompatibleEnchantments()) {
+                if (combinedEnchantments.containsKey(incompatible)) {
+                    canApply = false;
+                    if (!bedrock) {
+                        cost++;
+                    }
+                }
+            }
+
+            if (canApply || (!bedrock && session.getGameMode() == GameMode.CREATIVE)) {
+                int currentLevel = combinedEnchantments.getOrDefault(enchantment, 0);
+                int newLevel = entry.getIntValue();
+                if (newLevel == currentLevel) {
+                    newLevel++;
+                }
+                newLevel = Math.max(currentLevel, newLevel);
+                if (newLevel > data.maxLevel()) {
+                    newLevel = data.maxLevel();
+                }
+                combinedEnchantments.put(enchantment, newLevel);
+
+                int rarityMultiplier = data.rarityMultiplier();
+                if (isEnchantedBook(session, material) && rarityMultiplier > 1) {
+                    rarityMultiplier /= 2;
+                }
+                if (bedrock) {
+                    if (newLevel > currentLevel) {
+                        hasCompatible = true;
+                    }
+                    if (enchantment == JavaEnchantment.IMPALING) {
+                        // Multiplier is halved on Bedrock for some reason
+                        rarityMultiplier /= 2;
+                    } else if (enchantment == JavaEnchantment.SWEEPING) {
+                        // Doesn't exist on Bedrock
+                        rarityMultiplier = 0;
+                    }
+                    cost += rarityMultiplier * (newLevel - currentLevel);
+                } else {
+                    hasCompatible = true;
+                    cost += rarityMultiplier * newLevel;
+                }
+            }
+        }
+
+        if (!hasCompatible) {
+            return -1;
+        }
+        return cost;
+    }
+
+    private Object2IntMap<JavaEnchantment> getEnchantments(GeyserSession session, GeyserItemStack itemStack, boolean bedrock) {
+        if (itemStack.getNbt() == null) {
+            return Object2IntMaps.emptyMap();
+        }
+        Object2IntMap<JavaEnchantment> enchantments = new Object2IntOpenHashMap<>();
+        Tag enchantmentTag;
+        if (isEnchantedBook(session, itemStack)) {
+            enchantmentTag = itemStack.getNbt().get("StoredEnchantments");
+        } else {
+            enchantmentTag = itemStack.getNbt().get("Enchantments");
+        }
+        if (enchantmentTag instanceof ListTag listTag) {
+            for (Tag tag : listTag.getValue()) {
+                if (tag instanceof CompoundTag enchantTag) {
+                    if (enchantTag.get("id") instanceof StringTag javaEnchId) {
+                        JavaEnchantment enchantment = JavaEnchantment.getByJavaIdentifier(javaEnchId.getValue());
+                        if (enchantment == null) {
+                            GeyserConnector.getInstance().getLogger().debug("Unknown java enchantment: " + javaEnchId.getValue());
+                            continue;
+                        }
+
+                        Tag javaEnchLvl = enchantTag.get("lvl");
+                        if (!(javaEnchLvl instanceof ShortTag || javaEnchLvl instanceof IntTag))
+                            continue;
+
+                        // Handle duplicate enchantments
+                        if (bedrock) {
+                            enchantments.putIfAbsent(enchantment, ((Number) javaEnchLvl.getValue()).intValue());
+                        } else {
+                            enchantments.mergeInt(enchantment, ((Number) javaEnchLvl.getValue()).intValue(), Math::max);
+                        }
+                    }
+                }
+            }
+        }
+        return enchantments;
+    }
+
+    private boolean isEnchantedBook(GeyserSession session, GeyserItemStack itemStack) {
+        return itemStack.getJavaId() == session.getItemMappings().getStoredItems().enchantedBook().getJavaId();
+    }
+
+    private boolean isCombining(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+        return isEnchantedBook(session, material) || (input.getJavaId() == material.getJavaId() && hasDurability(session, input));
+    }
+
+    private boolean isRepairing(GeyserSession session, GeyserItemStack input, GeyserItemStack material) {
+        Set<String> repairMaterials = input.getMapping(session).getRepairMaterials();
+        return repairMaterials != null && repairMaterials.contains(material.getMapping(session).getJavaIdentifier());
+    }
+
+    private boolean isRenaming(GeyserSession session, AnvilContainer anvilContainer, boolean bedrock) {
+        if (anvilContainer.getResult().isEmpty()) {
+            return false;
+        }
+        // This should really check the name field in all cases, but that requires the localized name
+        // of the item which can change depending on NBT and Minecraft Edition
+        String originalName = ItemUtils.getCustomName(anvilContainer.getInput().getNbt());
+        if (bedrock && originalName != null && anvilContainer.getNewName() != null) {
+            // Check text and formatting
+            String legacyOriginalName = MessageTranslator.convertMessageLenient(originalName, session.getLocale());
+            return !legacyOriginalName.equals(anvilContainer.getNewName());
+        }
+        return !Objects.equals(originalName, ItemUtils.getCustomName(anvilContainer.getResult().getNbt()));
+    }
+
+    private int getTagIntValueOr(GeyserItemStack itemStack, String tagName, int defaultValue) {
+        if (itemStack.getNbt() != null) {
+            Tag tag = itemStack.getNbt().get(tagName);
+            if (tag != null && tag.getValue() instanceof Number value) {
+                return value.intValue();
+            }
+        }
+        return defaultValue;
+    }
+
+    private int getRepairCost(GeyserItemStack itemStack) {
+        return getTagIntValueOr(itemStack, "RepairCost", 0);
+    }
+
+    private boolean hasDurability(GeyserSession session, GeyserItemStack itemStack) {
+        if (itemStack.getMapping(session).getMaxDamage() > 0) {
+            return getTagIntValueOr(itemStack, "Unbreakable", 0) == 0;
+        }
+        return false;
+    }
+
+    private int getDamage(GeyserItemStack itemStack) {
+        return getTagIntValueOr(itemStack, "Damage", 0);
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
index 14b918a4f..947f9e6ed 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/Enchantment.java
@@ -147,6 +147,18 @@ public enum Enchantment {
          */
         public static final String[] ALL_JAVA_IDENTIFIERS;
 
+        public static JavaEnchantment getByJavaIdentifier(String javaIdentifier) {
+            if (!javaIdentifier.startsWith("minecraft:")) {
+                javaIdentifier = "minecraft:" + javaIdentifier;
+            }
+            for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {
+                if (ALL_JAVA_IDENTIFIERS[i].equalsIgnoreCase(javaIdentifier)) {
+                    return VALUES[i];
+                }
+            }
+            return null;
+        }
+
         static {
             ALL_JAVA_IDENTIFIERS = new String[VALUES.length];
             for (int i = 0; i < ALL_JAVA_IDENTIFIERS.length; i++) {
diff --git a/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java b/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java
index 7f8456d6a..6bbdb7421 100644
--- a/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java
+++ b/connector/src/main/java/org/geysermc/connector/network/translators/item/StoredItemMappings.java
@@ -43,6 +43,7 @@ public class StoredItemMappings {
     private final ItemMapping barrier;
     private final ItemMapping compass;
     private final ItemMapping crossbow;
+    private final ItemMapping enchantedBook;
     private final ItemMapping fishingRod;
     private final ItemMapping lodestoneCompass;
     private final ItemMapping milkBucket;
@@ -58,6 +59,7 @@ public class StoredItemMappings {
         this.barrier = load(itemMappings, "barrier");
         this.compass = load(itemMappings, "compass");
         this.crossbow = load(itemMappings, "crossbow");
+        this.enchantedBook = load(itemMappings, "enchanted_book");
         this.fishingRod = load(itemMappings, "fishing_rod");
         this.lodestoneCompass = load(itemMappings, "lodestone_compass");
         this.milkBucket = load(itemMappings, "milk_bucket");
diff --git a/connector/src/main/java/org/geysermc/connector/registry/Registries.java b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
index 3447bdfc8..43b28a530 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/Registries.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/Registries.java
@@ -39,12 +39,14 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import org.geysermc.connector.network.translators.collision.translators.BlockCollision;
 import org.geysermc.connector.network.translators.effect.Effect;
+import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
 import org.geysermc.connector.network.translators.sound.SoundHandler;
 import org.geysermc.connector.network.translators.sound.SoundInteractionHandler;
 import org.geysermc.connector.network.translators.world.block.entity.BlockEntityTranslator;
 import org.geysermc.connector.registry.loader.*;
 import org.geysermc.connector.registry.populator.ItemRegistryPopulator;
 import org.geysermc.connector.registry.populator.RecipeRegistryPopulator;
+import org.geysermc.connector.registry.type.EnchantmentData;
 import org.geysermc.connector.registry.type.ItemMappings;
 import org.geysermc.connector.registry.type.ParticleMapping;
 import org.geysermc.connector.registry.type.SoundMapping;
@@ -82,6 +84,11 @@ public class Registries {
      */
     public static final VersionedRegistry<Map<RecipeType, List<CraftingData>>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
 
+    /**
+     * A registry holding data of all the known enchantments.
+     */
+    public static final SimpleMappedRegistry<JavaEnchantment, EnchantmentData> ENCHANTMENTS;
+
     /**
      * A registry holding a CompoundTag of the known entity identifiers.
      */
@@ -140,5 +147,6 @@ public class Registries {
 
         // Create registries that require other registries to load first
         POTION_MIXES = SimpleRegistry.create(PotionMixRegistryLoader::new);
+        ENCHANTMENTS = SimpleMappedRegistry.create("mappings/enchantments.json", EnchantmentRegistryLoader::new);
     }
 }
\ No newline at end of file
diff --git a/connector/src/main/java/org/geysermc/connector/registry/loader/EnchantmentRegistryLoader.java b/connector/src/main/java/org/geysermc/connector/registry/loader/EnchantmentRegistryLoader.java
new file mode 100644
index 000000000..d1a100a55
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/registry/loader/EnchantmentRegistryLoader.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.registry.loader;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntSet;
+import org.geysermc.connector.GeyserConnector;
+import org.geysermc.connector.network.BedrockProtocol;
+import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
+import org.geysermc.connector.registry.Registries;
+import org.geysermc.connector.registry.type.EnchantmentData;
+import org.geysermc.connector.registry.type.ItemMapping;
+import org.geysermc.connector.utils.FileUtils;
+
+import java.io.InputStream;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.Map;
+
+public class EnchantmentRegistryLoader implements RegistryLoader<String, Map<JavaEnchantment, EnchantmentData>> {
+    @Override
+    public Map<JavaEnchantment, EnchantmentData> load(String input) {
+        InputStream enchantmentsStream = FileUtils.getResource(input);
+        JsonNode enchantmentsNode;
+        try {
+            enchantmentsNode = GeyserConnector.JSON_MAPPER.readTree(enchantmentsStream);
+        } catch (Exception e) {
+            throw new AssertionError("Unable to load enchantment data", e);
+        }
+
+        Map<JavaEnchantment, EnchantmentData> enchantments = new EnumMap<>(JavaEnchantment.class);
+        Iterator<Map.Entry<String, JsonNode>> it = enchantmentsNode.fields();
+        while (it.hasNext()) {
+            Map.Entry<String, JsonNode> entry = it.next();
+            JavaEnchantment key = JavaEnchantment.getByJavaIdentifier(entry.getKey());
+            JsonNode node = entry.getValue();
+            int rarityMultiplier = switch (node.get("rarity").textValue()) {
+                case "common" -> 1;
+                case "uncommon" -> 2;
+                case "rare" -> 4;
+                case "very_rare" -> 8;
+                default -> throw new IllegalStateException("Unexpected value: " + node.get("rarity").textValue());
+            };
+            int maxLevel = node.get("max_level").asInt();
+
+            EnumSet<JavaEnchantment> incompatibleEnchantments = EnumSet.noneOf(JavaEnchantment.class);
+            JsonNode incompatibleEnchantmentsNode = node.get("incompatible_enchantments");
+            if (incompatibleEnchantmentsNode != null) {
+                for (JsonNode incompatibleNode : incompatibleEnchantmentsNode) {
+                    incompatibleEnchantments.add(JavaEnchantment.getByJavaIdentifier(incompatibleNode.textValue()));
+                }
+            }
+
+            IntSet validItems = new IntOpenHashSet();
+            for (JsonNode itemNode : node.get("valid_items")) {
+                String javaIdentifier = itemNode.textValue();
+                ItemMapping itemMapping = Registries.ITEMS.forVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getMapping(javaIdentifier);
+                if (itemMapping != null) {
+                    validItems.add(itemMapping.getJavaId());
+                } else {
+                    throw new NullPointerException("No item entry exists for java identifier: " + javaIdentifier);
+                }
+            }
+
+            EnchantmentData enchantmentData = new EnchantmentData(rarityMultiplier, maxLevel, incompatibleEnchantments, validItems);
+            enchantments.put(key, enchantmentData);
+        }
+        return enchantments;
+    }
+}
diff --git a/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java b/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java
index 0251103d0..e1b6a45b6 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/populator/ItemRegistryPopulator.java
@@ -38,10 +38,7 @@ import com.nukkitx.protocol.bedrock.packet.StartGamePacket;
 import com.nukkitx.protocol.bedrock.v448.Bedrock_v448;
 import com.nukkitx.protocol.bedrock.v465.Bedrock_v465;
 import com.nukkitx.protocol.bedrock.v471.Bedrock_v471;
-import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
-import it.unimi.dsi.fastutil.ints.IntArrayList;
-import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.ints.*;
 import it.unimi.dsi.fastutil.objects.*;
 import org.geysermc.connector.GeyserConnector;
 import org.geysermc.connector.network.translators.item.StoredItemMappings;
@@ -377,7 +374,12 @@ public class ItemRegistryPopulator {
                         .bedrockId(bedrockId)
                         .bedrockData(mappingItem.getBedrockData())
                         .bedrockBlockId(bedrockBlockId)
-                        .stackSize(stackSize);
+                        .stackSize(stackSize)
+                        .maxDamage(mappingItem.getMaxDamage());
+
+                if (mappingItem.getRepairMaterials() != null) {
+                    mappingBuilder = mappingBuilder.repairMaterials(new ObjectOpenHashSet<>(mappingItem.getRepairMaterials()));
+                }
 
                 if (mappingItem.getToolType() != null) {
                     if (mappingItem.getToolTier() != null) {
diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/EnchantmentData.java b/connector/src/main/java/org/geysermc/connector/registry/type/EnchantmentData.java
new file mode 100644
index 000000000..cd16a093a
--- /dev/null
+++ b/connector/src/main/java/org/geysermc/connector/registry/type/EnchantmentData.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.connector.registry.type;
+
+import it.unimi.dsi.fastutil.ints.IntSet;
+import org.geysermc.connector.network.translators.item.Enchantment.JavaEnchantment;
+
+import java.util.Set;
+
+public record EnchantmentData(int rarityMultiplier, int maxLevel, Set<JavaEnchantment> incompatibleEnchantments,
+                              IntSet validItems) {
+}
diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java b/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java
index da91c412e..12e5544b7 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/type/GeyserMappingItem.java
@@ -28,6 +28,8 @@ package org.geysermc.connector.registry.type;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 
+import java.util.List;
+
 /**
  * Represents Geyser's own serialized item information before being processed per-version
  */
@@ -40,4 +42,6 @@ public class GeyserMappingItem {
     @JsonProperty("stack_size") int stackSize = 64;
     @JsonProperty("tool_type") String toolType;
     @JsonProperty("tool_tier") String toolTier;
+    @JsonProperty("max_damage") int maxDamage = 0;
+    @JsonProperty("repair_materials") List<String> repairMaterials;
 }
diff --git a/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java b/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java
index fff00950b..46789a69a 100644
--- a/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java
+++ b/connector/src/main/java/org/geysermc/connector/registry/type/ItemMapping.java
@@ -31,13 +31,15 @@ import lombok.Value;
 import org.geysermc.connector.network.BedrockProtocol;
 import org.geysermc.connector.registry.BlockRegistries;
 
+import java.util.Set;
+
 @Value
 @Builder
 @EqualsAndHashCode
 public class ItemMapping {
     public static final ItemMapping AIR = new ItemMapping("minecraft:air", "minecraft:air", 0, 0, 0,
             BlockRegistries.BLOCKS.forVersion(BedrockProtocol.DEFAULT_BEDROCK_CODEC.getProtocolVersion()).getBedrockAirId(),
-            64, null, null, null);
+            64, null, null, null, 0, null);
 
     String javaIdentifier;
     String bedrockIdentifier;
@@ -57,6 +59,10 @@ public class ItemMapping {
 
     String translationString;
 
+    int maxDamage;
+
+    Set<String> repairMaterials;
+
     /**
      * Gets if this item is a block.
      *
diff --git a/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java b/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java
index db4e9e2e1..8bd2edfd1 100644
--- a/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java
+++ b/connector/src/main/java/org/geysermc/connector/utils/ItemUtils.java
@@ -58,4 +58,19 @@ public class ItemUtils {
         }
         return original;
     }
+
+    /**
+     * @param itemTag the NBT tag of the item
+     * @return the custom name of the item
+     */
+    public static String getCustomName(CompoundTag itemTag) {
+        if (itemTag != null) {
+            if (itemTag.get("display") instanceof CompoundTag displayTag) {
+                if (displayTag.get("Name") instanceof StringTag nameTag) {
+                    return nameTag.getValue();
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/connector/src/main/resources/mappings b/connector/src/main/resources/mappings
index 7ff1b6567..5b6239f0a 160000
--- a/connector/src/main/resources/mappings
+++ b/connector/src/main/resources/mappings
@@ -1 +1 @@
-Subproject commit 7ff1b6567b56c7b0b8e28786b9bbc30abfaededf
+Subproject commit 5b6239f0a43ec9a38d65ed53b8d1bfaf564c1c3b
diff --git a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java
index 649e96425..cfb2bbc97 100644
--- a/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java
+++ b/connector/src/test/java/org/geysermc/connector/network/translators/chat/MessageTranslatorTest.java
@@ -81,6 +81,16 @@ public class MessageTranslatorTest {
         Assert.assertEquals("Unimplemented formatting chars not stripped", "Bold Underline", MessageTranslator.convertMessageLenient("§m§nBold Underline"));
     }
 
+    @Test
+    public void convertToPlainText() {
+        Assert.assertEquals("JSON message is not handled properly", "Many colors here", MessageTranslator.convertToPlainText("{\"extra\":[{\"color\":\"red\",\"text\":\"M\"},{\"color\":\"gold\",\"text\":\"a\"},{\"color\":\"yellow\",\"text\":\"n\"},{\"color\":\"green\",\"text\":\"y \"},{\"color\":\"aqua\",\"text\":\"c\"},{\"color\":\"dark_purple\",\"text\":\"o\"},{\"color\":\"red\",\"text\":\"l\"},{\"color\":\"gold\",\"text\":\"o\"},{\"color\":\"yellow\",\"text\":\"r\"},{\"color\":\"green\",\"text\":\"s \"},{\"color\":\"aqua\",\"text\":\"h\"},{\"color\":\"dark_purple\",\"text\":\"e\"},{\"color\":\"red\",\"text\":\"r\"},{\"color\":\"gold\",\"text\":\"e\"}],\"text\":\"\"}", "en_US"));
+        Assert.assertEquals("Legacy formatted message is not handled properly (Colors)", "Many colors here", MessageTranslator.convertToPlainText("§cM§6a§en§ay §bc§5o§cl§6o§er§as §bh§5e§cr§6e", "en_US"));
+        Assert.assertEquals("Legacy formatted message is not handled properly (Style)", "Obf Bold Strikethrough Underline Italic Reset", MessageTranslator.convertToPlainText("§kObf §lBold §mStrikethrough §nUnderline §oItalic §rReset", "en_US"));
+        Assert.assertEquals("Valid lenient JSON is not handled properly", "Strange", MessageTranslator.convertToPlainText("§rStrange", "en_US"));
+        Assert.assertEquals("Empty message is not handled properly", "", MessageTranslator.convertToPlainText("", "en_US"));
+        Assert.assertEquals("Whitespace is not preserved", "     ", MessageTranslator.convertToPlainText("     ", "en_US"));
+    }
+
     @Test
     public void testNullTextPacket() {
         DefaultComponentSerializer.get().deserialize("null");